blob: 727c80b50a67404ffedeaf320ab5897291add123 [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/media/audio_stream_monitor.h"
#include <map>
#include <memory>
#include <utility>
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/invalidate_type.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
#include "media/base/audio_power_monitor.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using ::testing::InvokeWithoutArgs;
namespace content {
namespace {
const GlobalRenderFrameHostId kRenderFrameHostId = {1, 2};
const GlobalRenderFrameHostId kRenderFrameHostIdSameProcess = {1, 3};
const GlobalRenderFrameHostId kRenderFrameHostIdOtherProcess = {4, 5};
const int kStreamId = 6;
const int kAnotherStreamId = 7;
// Used to confirm audio indicator state changes occur at the correct times.
class MockWebContentsDelegate : public WebContentsDelegate {
public:
MOCK_METHOD2(NavigationStateChanged,
void(WebContents* source, InvalidateTypes changed_flags));
};
} // namespace
class AudioStreamMonitorTest : public RenderViewHostTestHarness {
public:
AudioStreamMonitorTest()
: RenderViewHostTestHarness(
base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
// Start time at non-zero.
task_environment()->FastForwardBy(base::Seconds(1000000));
}
AudioStreamMonitorTest(const AudioStreamMonitorTest&) = delete;
AudioStreamMonitorTest& operator=(const AudioStreamMonitorTest&) = delete;
void SetUp() override {
RenderViewHostTestHarness::SetUp();
WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(
RenderViewHostTestHarness::web_contents());
web_contents->SetDelegate(&mock_web_contents_delegate_);
monitor_ = web_contents->audio_stream_monitor();
}
void TearDown() override {
monitor_ = nullptr;
RenderViewHostTestHarness::TearDown();
}
void FastForwardBy(const base::TimeDelta& delta) {
task_environment()->FastForwardBy(delta);
}
void ExpectIsMonitoring(GlobalRenderFrameHostId render_frame_host_id,
int stream_id,
bool is_polling) {
const AudioStreamMonitor::StreamID key = {render_frame_host_id, stream_id};
EXPECT_EQ(is_polling,
monitor_->streams_.find(key) != monitor_->streams_.end());
}
void ExpectTabWasRecentlyAudible(
bool was_audible,
const base::TimeTicks& last_became_silent_time) {
EXPECT_EQ(was_audible, monitor_->indicator_is_on_);
EXPECT_EQ(last_became_silent_time, monitor_->last_became_silent_time_);
EXPECT_EQ(monitor_->off_timer_.IsRunning(),
monitor_->indicator_is_on_ && !monitor_->IsCurrentlyAudible() &&
base::TimeTicks::Now() <
monitor_->last_became_silent_time_ + holding_period());
}
void ExpectIsCurrentlyAudible() const {
EXPECT_TRUE(monitor_->IsCurrentlyAudible());
}
void ExpectNotCurrentlyAudible() const {
EXPECT_FALSE(monitor_->IsCurrentlyAudible());
}
void ExpectRecentlyAudibleChangeNotification(bool new_recently_audible) {
EXPECT_CALL(
mock_web_contents_delegate_,
NavigationStateChanged(RenderViewHostTestHarness::web_contents(),
INVALIDATE_TYPE_AUDIO))
.WillOnce(InvokeWithoutArgs(
this, new_recently_audible
? &AudioStreamMonitorTest::ExpectWasRecentlyAudible
: &AudioStreamMonitorTest::ExpectNotRecentlyAudible))
.RetiresOnSaturation();
}
void ExpectCurrentlyAudibleChangeNotification(bool new_audible) {
EXPECT_CALL(
mock_web_contents_delegate_,
NavigationStateChanged(RenderViewHostTestHarness::web_contents(),
INVALIDATE_TYPE_AUDIO))
.WillOnce(InvokeWithoutArgs(
this, new_audible
? &AudioStreamMonitorTest::ExpectIsCurrentlyAudible
: &AudioStreamMonitorTest::ExpectNotCurrentlyAudible))
.RetiresOnSaturation();
}
// A small time step useful for testing the passage of time.
static base::TimeDelta one_time_step() { return base::Seconds(1) / 15; }
static base::TimeDelta holding_period() {
return base::Milliseconds(AudioStreamMonitor::kHoldOnMilliseconds);
}
void StartMonitoring(GlobalRenderFrameHostId render_frame_host_id,
int stream_id) {
monitor_->StartMonitoringStreamOnUIThread(
AudioStreamMonitor::StreamID{render_frame_host_id, stream_id});
}
void StopMonitoring(GlobalRenderFrameHostId render_frame_host_id,
int stream_id) {
monitor_->StopMonitoringStreamOnUIThread(
AudioStreamMonitor::StreamID{render_frame_host_id, stream_id});
}
void UpdateAudibleState(GlobalRenderFrameHostId render_frame_host_id,
int stream_id,
bool is_audible) {
monitor_->UpdateStreamAudibleStateOnUIThread(
AudioStreamMonitor::StreamID{render_frame_host_id, stream_id},
is_audible);
}
WebContents* web_contents() { return monitor_->web_contents_; }
protected:
raw_ptr<AudioStreamMonitor> monitor_;
private:
void ExpectWasRecentlyAudible() const {
EXPECT_TRUE(monitor_->WasRecentlyAudible());
}
void ExpectNotRecentlyAudible() const {
EXPECT_FALSE(monitor_->WasRecentlyAudible());
}
MockWebContentsDelegate mock_web_contents_delegate_;
};
TEST_F(AudioStreamMonitorTest, MonitorsWhenProvidedAStream) {
EXPECT_FALSE(monitor_->WasRecentlyAudible());
ExpectNotCurrentlyAudible();
ExpectIsMonitoring(kRenderFrameHostId, kStreamId, false);
StartMonitoring(kRenderFrameHostId, kStreamId);
EXPECT_FALSE(monitor_->WasRecentlyAudible());
ExpectNotCurrentlyAudible();
ExpectIsMonitoring(kRenderFrameHostId, kStreamId, true);
StopMonitoring(kRenderFrameHostId, kStreamId);
EXPECT_FALSE(monitor_->WasRecentlyAudible());
ExpectNotCurrentlyAudible();
ExpectIsMonitoring(kRenderFrameHostId, kStreamId, false);
}
// Tests that AudioStreamMonitor keeps the indicator on for the holding period
// even if there is silence during the holding period.
// See comments in audio_stream_monitor.h for expected behavior.
TEST_F(AudioStreamMonitorTest, IndicatorIsOnUntilHoldingPeriodHasPassed) {
StartMonitoring(kRenderFrameHostId, kStreamId);
// Expect WebContents will get one call form AudioStreamMonitor to toggle the
// indicator upon the very first notification that the stream has become
// audible.
ExpectRecentlyAudibleChangeNotification(true);
// Loop, each time testing a slightly longer period of silence. The recently
// audible state should not change while the currently audible one should.
int num_silence_steps = 1;
base::TimeTicks last_became_silent_time;
do {
ExpectCurrentlyAudibleChangeNotification(true);
UpdateAudibleState(kRenderFrameHostId, kStreamId, true);
ExpectTabWasRecentlyAudible(true, last_became_silent_time);
FastForwardBy(one_time_step());
ExpectCurrentlyAudibleChangeNotification(false);
// Notify that the stream has become silent and advance time repeatedly,
// ensuring that the indicator is being held on during the holding period.
UpdateAudibleState(kRenderFrameHostId, kStreamId, false);
last_became_silent_time = base::TimeTicks::Now();
ExpectTabWasRecentlyAudible(true, last_became_silent_time);
for (int i = 0; i < num_silence_steps; ++i) {
// If the next time step will cause the holding period to expire, then a
// notification will be sent.
if (base::TimeTicks::Now() + one_time_step() >=
last_became_silent_time + holding_period()) {
ExpectRecentlyAudibleChangeNotification(false);
}
ExpectTabWasRecentlyAudible(true, last_became_silent_time);
FastForwardBy(one_time_step());
}
++num_silence_steps;
} while (base::TimeTicks::Now() < last_became_silent_time + holding_period());
// At this point, the time has just advanced to beyond the holding period and
// the tab indicator has been turned off. Also, make sure it stays off for
// several cycles thereafter.
for (int i = 0; i < 10; ++i) {
ExpectTabWasRecentlyAudible(false, last_became_silent_time);
FastForwardBy(one_time_step());
}
}
// Tests that the AudioStreamMonitor correctly processes updates from two
// different streams in the same tab.
TEST_F(AudioStreamMonitorTest, HandlesMultipleStreamUpdate) {
StartMonitoring(kRenderFrameHostId, kStreamId);
StartMonitoring(kRenderFrameHostIdSameProcess, kAnotherStreamId);
base::TimeTicks last_became_silent_time;
ExpectTabWasRecentlyAudible(false, last_became_silent_time);
ExpectNotCurrentlyAudible();
// The first stream becomes audible and the second stream is silent. The tab
// becomes audible.
ExpectRecentlyAudibleChangeNotification(true);
ExpectCurrentlyAudibleChangeNotification(true);
UpdateAudibleState(kRenderFrameHostId, kStreamId, true);
UpdateAudibleState(kRenderFrameHostIdSameProcess, kAnotherStreamId, false);
ExpectTabWasRecentlyAudible(true, last_became_silent_time);
ExpectIsCurrentlyAudible();
// Halfway through the holding period, the second stream joins in. The
// indicator stays on.
FastForwardBy(holding_period() / 2);
UpdateAudibleState(kRenderFrameHostIdSameProcess, kAnotherStreamId, true);
ExpectTabWasRecentlyAudible(true, last_became_silent_time);
ExpectIsCurrentlyAudible();
// Now, both streams become silent. The tab becoms silent but the indicator
// stays on.
ExpectCurrentlyAudibleChangeNotification(false);
UpdateAudibleState(kRenderFrameHostId, kStreamId, false);
UpdateAudibleState(kRenderFrameHostIdSameProcess, kAnotherStreamId, false);
last_became_silent_time = base::TimeTicks::Now();
ExpectNotCurrentlyAudible();
ExpectTabWasRecentlyAudible(true, last_became_silent_time);
// Advance half a holding period and the indicator should still be on.
FastForwardBy(holding_period() / 2);
ExpectTabWasRecentlyAudible(true, last_became_silent_time);
ExpectNotCurrentlyAudible();
// The first stream becomes audible again during the holding period.
// The tab becomes audible and the indicator stays on.
ExpectCurrentlyAudibleChangeNotification(true);
UpdateAudibleState(kRenderFrameHostId, kStreamId, true);
ExpectTabWasRecentlyAudible(true, last_became_silent_time);
ExpectIsCurrentlyAudible();
// Advance a holding period. The original holding period has expired but the
// indicator should stay on because a stream became audible in the meantime.
FastForwardBy(holding_period() / 2);
ExpectTabWasRecentlyAudible(true, last_became_silent_time);
ExpectIsCurrentlyAudible();
// The first stream becomes silent again. The tab becomes silent and the
// indicator is still on.
ExpectCurrentlyAudibleChangeNotification(false);
UpdateAudibleState(kRenderFrameHostId, kStreamId, false);
last_became_silent_time = base::TimeTicks::Now();
ExpectTabWasRecentlyAudible(true, last_became_silent_time);
ExpectNotCurrentlyAudible();
// After a holding period passes, the indicator turns off.
ExpectRecentlyAudibleChangeNotification(false);
FastForwardBy(holding_period());
ExpectTabWasRecentlyAudible(false, last_became_silent_time);
ExpectNotCurrentlyAudible();
// Now, the second stream becomes audible and the first one remains silent.
// The tab becomes audible again.
ExpectRecentlyAudibleChangeNotification(true);
ExpectCurrentlyAudibleChangeNotification(true);
UpdateAudibleState(kRenderFrameHostIdSameProcess, kAnotherStreamId, true);
ExpectTabWasRecentlyAudible(true, last_became_silent_time);
ExpectIsCurrentlyAudible();
// From here onwards, both streams are silent. Halfway through the holding
// period, the tab is no longer audible but stays as recently audible.
ExpectCurrentlyAudibleChangeNotification(false);
UpdateAudibleState(kRenderFrameHostIdSameProcess, kAnotherStreamId, false);
last_became_silent_time = base::TimeTicks::Now();
FastForwardBy(holding_period() / 2);
ExpectTabWasRecentlyAudible(true, last_became_silent_time);
ExpectNotCurrentlyAudible();
// Just past the holding period, the tab is no longer marked as recently
// audible.
ExpectRecentlyAudibleChangeNotification(false);
FastForwardBy(holding_period() -
(base::TimeTicks::Now() - last_became_silent_time));
ExpectTabWasRecentlyAudible(false, last_became_silent_time);
ExpectNotCurrentlyAudible();
// The passage of time should not turn the indicator back while both streams
// are remaining silent.
for (int i = 0; i < 100; ++i) {
FastForwardBy(one_time_step());
ExpectTabWasRecentlyAudible(false, last_became_silent_time);
ExpectNotCurrentlyAudible();
}
}
TEST_F(AudioStreamMonitorTest, MultipleRendererProcesses) {
StartMonitoring(kRenderFrameHostId, kStreamId);
StartMonitoring(kRenderFrameHostIdOtherProcess, kStreamId);
ExpectIsMonitoring(kRenderFrameHostId, kStreamId, true);
ExpectIsMonitoring(kRenderFrameHostIdOtherProcess, kStreamId, true);
StopMonitoring(kRenderFrameHostIdOtherProcess, kStreamId);
ExpectIsMonitoring(kRenderFrameHostId, kStreamId, true);
ExpectIsMonitoring(kRenderFrameHostIdOtherProcess, kStreamId, false);
}
TEST_F(AudioStreamMonitorTest, RenderProcessGone) {
StartMonitoring(kRenderFrameHostId, kStreamId);
StartMonitoring(kRenderFrameHostIdOtherProcess, kStreamId);
ExpectIsMonitoring(kRenderFrameHostId, kStreamId, true);
ExpectIsMonitoring(kRenderFrameHostIdOtherProcess, kStreamId, true);
monitor_->RenderProcessGone(kRenderFrameHostId.child_id);
ExpectIsMonitoring(kRenderFrameHostId, kStreamId, false);
monitor_->RenderProcessGone(kRenderFrameHostIdOtherProcess.child_id);
ExpectIsMonitoring(kRenderFrameHostIdOtherProcess, kStreamId, false);
}
TEST_F(AudioStreamMonitorTest, RenderFrameGone) {
RenderFrameHost* render_frame_host = web_contents()->GetPrimaryMainFrame();
GlobalRenderFrameHostId render_frame_host_id =
render_frame_host->GetGlobalId();
StartMonitoring(render_frame_host_id, kStreamId);
StartMonitoring(kRenderFrameHostIdOtherProcess, kStreamId);
ExpectIsMonitoring(render_frame_host_id, kStreamId, true);
ExpectIsMonitoring(kRenderFrameHostIdOtherProcess, kStreamId, true);
monitor_->RenderFrameDeleted(render_frame_host);
ExpectIsMonitoring(render_frame_host_id, kStreamId, false);
ExpectIsMonitoring(kRenderFrameHostIdOtherProcess, kStreamId, true);
}
TEST_F(AudioStreamMonitorTest, OneAudibleClient) {
ExpectNotCurrentlyAudible();
auto* render_frame_host_impl =
static_cast<RenderFrameHostImpl*>(web_contents()->GetPrimaryMainFrame());
GlobalRenderFrameHostId host_id = render_frame_host_impl->GetGlobalId();
ExpectRecentlyAudibleChangeNotification(true);
ExpectCurrentlyAudibleChangeNotification(true);
auto registration = monitor_->RegisterAudibleClient(host_id);
ExpectIsCurrentlyAudible();
ExpectCurrentlyAudibleChangeNotification(false);
registration.reset();
ExpectNotCurrentlyAudible();
}
TEST_F(AudioStreamMonitorTest, MultipleAudibleClients) {
ExpectNotCurrentlyAudible();
auto* render_frame_host_impl =
static_cast<RenderFrameHostImpl*>(web_contents()->GetPrimaryMainFrame());
GlobalRenderFrameHostId host_id = render_frame_host_impl->GetGlobalId();
// Add one client and the tab becomes audible.
ExpectRecentlyAudibleChangeNotification(true);
ExpectCurrentlyAudibleChangeNotification(true);
auto registration1 = monitor_->RegisterAudibleClient(host_id);
ExpectIsCurrentlyAudible();
// Add another client and the tab remains audible.
auto registration2 = monitor_->RegisterAudibleClient(host_id);
ExpectIsCurrentlyAudible();
// Removes one client and the tab remains audible.
registration1.reset();
ExpectIsCurrentlyAudible();
// Removes another client and the tab is not audible.
ExpectCurrentlyAudibleChangeNotification(false);
registration2.reset();
ExpectNotCurrentlyAudible();
}
TEST_F(AudioStreamMonitorTest, MultipleAudibleClientsMultipleRenderFrames) {
ExpectNotCurrentlyAudible();
// We need to navigate once before we can add child frames.
const char kDefaultTestUrl[] = "https://google.com/";
content::NavigationSimulator::NavigateAndCommitFromBrowser(
web_contents(), GURL(kDefaultTestUrl));
auto* render_frame_host_tester =
RenderFrameHostTester::For(web_contents()->GetPrimaryMainFrame());
auto* render_frame_host_impl_1 = static_cast<RenderFrameHostImpl*>(
render_frame_host_tester->AppendChild("child_1"));
auto* render_frame_host_impl_2 = static_cast<RenderFrameHostImpl*>(
render_frame_host_tester->AppendChild("child_2"));
GlobalRenderFrameHostId host_id1 = render_frame_host_impl_1->GetGlobalId();
GlobalRenderFrameHostId host_id2 = render_frame_host_impl_2->GetGlobalId();
// Add one client and the tab becomes audible.
ExpectRecentlyAudibleChangeNotification(true);
ExpectCurrentlyAudibleChangeNotification(true);
auto registration1 = monitor_->RegisterAudibleClient(host_id1);
ExpectIsCurrentlyAudible();
// Add another client and the tab remains audible.
auto registration2 = monitor_->RegisterAudibleClient(host_id2);
ExpectIsCurrentlyAudible();
// Removes one client and the tab remains audible.
registration1.reset();
ExpectIsCurrentlyAudible();
// Removes another client and the tab is not audible.
ExpectCurrentlyAudibleChangeNotification(false);
registration2.reset();
ExpectNotCurrentlyAudible();
}
TEST_F(AudioStreamMonitorTest, AudibleClientAndStream) {
StartMonitoring(kRenderFrameHostId, kStreamId);
ExpectNotCurrentlyAudible();
auto* render_frame_host_impl =
static_cast<RenderFrameHostImpl*>(web_contents()->GetPrimaryMainFrame());
GlobalRenderFrameHostId host_id = render_frame_host_impl->GetGlobalId();
// Add one client and the tab becomes audible.
ExpectRecentlyAudibleChangeNotification(true);
ExpectCurrentlyAudibleChangeNotification(true);
auto registration = monitor_->RegisterAudibleClient(host_id);
ExpectIsCurrentlyAudible();
// The stream becomes audible and the tab remains audible.
UpdateAudibleState(kRenderFrameHostId, kStreamId, true);
ExpectIsCurrentlyAudible();
// Remove the client and the tab remains audible.
registration.reset();
ExpectIsCurrentlyAudible();
// The stream becomes not audible and the tab is not audible.
ExpectCurrentlyAudibleChangeNotification(false);
UpdateAudibleState(kRenderFrameHostId, kStreamId, false);
ExpectNotCurrentlyAudible();
}
} // namespace content