| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // 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 <utility> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/memory/scoped_ptr.h" |
| #include "base/test/simple_test_tick_clock.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/test_renderer_host.h" |
| #include "media/audio/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 int kRenderProcessId = 1; |
| const int kAnotherRenderProcessId = 2; |
| const int kStreamId = 3; |
| const int kAnotherStreamId = 6; |
| |
| // Used to confirm audio indicator state changes occur at the correct times. |
| class MockWebContentsDelegate : public WebContentsDelegate { |
| public: |
| MOCK_METHOD2(NavigationStateChanged, |
| void(const WebContents* source, InvalidateTypes changed_flags)); |
| }; |
| |
| } // namespace |
| |
| class AudioStreamMonitorTest : public RenderViewHostTestHarness { |
| public: |
| AudioStreamMonitorTest() { |
| // Start |clock_| at non-zero. |
| clock_.Advance(base::TimeDelta::FromSeconds(1000000)); |
| } |
| |
| virtual void SetUp() override { |
| RenderViewHostTestHarness::SetUp(); |
| |
| WebContentsImpl* web_contents = reinterpret_cast<WebContentsImpl*>( |
| RenderViewHostTestHarness::web_contents()); |
| web_contents->SetDelegate(&mock_web_contents_delegate_); |
| monitor_ = web_contents->audio_stream_monitor(); |
| const_cast<base::TickClock*&>(monitor_->clock_) = &clock_; |
| } |
| |
| base::TimeTicks GetTestClockTime() { return clock_.NowTicks(); } |
| |
| void AdvanceClock(const base::TimeDelta& delta) { clock_.Advance(delta); } |
| |
| AudioStreamMonitor::ReadPowerAndClipCallback CreatePollCallback( |
| int stream_id) { |
| return base::Bind( |
| &AudioStreamMonitorTest::ReadPower, base::Unretained(this), stream_id); |
| } |
| |
| void SetStreamPower(int stream_id, float power) { |
| current_power_[stream_id] = power; |
| } |
| |
| void SimulatePollTimerFired() { monitor_->Poll(); } |
| |
| void SimulateOffTimerFired() { monitor_->MaybeToggle(); } |
| |
| void ExpectIsPolling(int render_process_id, int stream_id, bool is_polling) { |
| const AudioStreamMonitor::StreamID key(render_process_id, stream_id); |
| EXPECT_EQ( |
| is_polling, |
| monitor_->poll_callbacks_.find(key) != monitor_->poll_callbacks_.end()); |
| EXPECT_EQ(!monitor_->poll_callbacks_.empty(), |
| monitor_->poll_timer_.IsRunning()); |
| } |
| |
| void ExpectTabWasRecentlyAudible(bool was_audible, |
| const base::TimeTicks& last_blurt_time) { |
| EXPECT_EQ(was_audible, monitor_->was_recently_audible_); |
| EXPECT_EQ(last_blurt_time, monitor_->last_blurt_time_); |
| EXPECT_EQ(monitor_->was_recently_audible_, |
| monitor_->off_timer_.IsRunning()); |
| } |
| |
| void ExpectWebContentsWillBeNotifiedOnce(bool should_be_audible) { |
| EXPECT_CALL( |
| mock_web_contents_delegate_, |
| NavigationStateChanged(RenderViewHostTestHarness::web_contents(), |
| INVALIDATE_TYPE_TAB)) |
| .WillOnce(InvokeWithoutArgs( |
| this, |
| should_be_audible |
| ? &AudioStreamMonitorTest::ExpectIsNotifyingForToggleOn |
| : &AudioStreamMonitorTest::ExpectIsNotifyingForToggleOff)) |
| .RetiresOnSaturation(); |
| } |
| |
| static base::TimeDelta one_polling_interval() { |
| return base::TimeDelta::FromSeconds(1) / |
| AudioStreamMonitor::kPowerMeasurementsPerSecond; |
| } |
| |
| static base::TimeDelta holding_period() { |
| return base::TimeDelta::FromMilliseconds( |
| AudioStreamMonitor::kHoldOnMilliseconds); |
| } |
| |
| void StartMonitoring( |
| int render_process_id, |
| int stream_id, |
| const AudioStreamMonitor::ReadPowerAndClipCallback& callback) { |
| monitor_->StartMonitoringStreamOnUIThread( |
| render_process_id, stream_id, callback); |
| } |
| |
| void StopMonitoring(int render_process_id, int stream_id) { |
| monitor_->StopMonitoringStreamOnUIThread(render_process_id, stream_id); |
| } |
| |
| protected: |
| AudioStreamMonitor* monitor_; |
| |
| private: |
| std::pair<float, bool> ReadPower(int stream_id) { |
| return std::make_pair(current_power_[stream_id], false); |
| } |
| |
| void ExpectIsNotifyingForToggleOn() { |
| EXPECT_TRUE(monitor_->WasRecentlyAudible()); |
| } |
| |
| void ExpectIsNotifyingForToggleOff() { |
| EXPECT_FALSE(monitor_->WasRecentlyAudible()); |
| } |
| |
| MockWebContentsDelegate mock_web_contents_delegate_; |
| base::SimpleTestTickClock clock_; |
| std::map<int, float> current_power_; |
| |
| DISALLOW_COPY_AND_ASSIGN(AudioStreamMonitorTest); |
| }; |
| |
| // Tests that AudioStreamMonitor is polling while it has a |
| // ReadPowerAndClipCallback, and is not polling at other times. |
| TEST_F(AudioStreamMonitorTest, PollsWhenProvidedACallback) { |
| EXPECT_FALSE(monitor_->WasRecentlyAudible()); |
| ExpectIsPolling(kRenderProcessId, kStreamId, false); |
| |
| StartMonitoring(kRenderProcessId, kStreamId, CreatePollCallback(kStreamId)); |
| EXPECT_FALSE(monitor_->WasRecentlyAudible()); |
| ExpectIsPolling(kRenderProcessId, kStreamId, true); |
| |
| StopMonitoring(kRenderProcessId, kStreamId); |
| EXPECT_FALSE(monitor_->WasRecentlyAudible()); |
| ExpectIsPolling(kRenderProcessId, kStreamId, false); |
| } |
| |
| // Tests that AudioStreamMonitor debounces the power level readings it's taking, |
| // which could be fluctuating rapidly between the audible versus silence |
| // threshold. See comments in audio_stream_monitor.h for expected behavior. |
| TEST_F(AudioStreamMonitorTest, |
| ImpulsesKeepIndicatorOnUntilHoldingPeriodHasPassed) { |
| StartMonitoring(kRenderProcessId, kStreamId, CreatePollCallback(kStreamId)); |
| |
| // Expect WebContents will get one call form AudioStreamMonitor to toggle the |
| // indicator on upon the very first poll. |
| ExpectWebContentsWillBeNotifiedOnce(true); |
| |
| // Loop, each time testing a slightly longer period of polled silence. The |
| // indicator should remain on throughout. |
| int num_silence_polls = 0; |
| base::TimeTicks last_blurt_time; |
| do { |
| // Poll an audible signal, and expect tab indicator state is on. |
| SetStreamPower(kStreamId, media::AudioPowerMonitor::max_power()); |
| last_blurt_time = GetTestClockTime(); |
| SimulatePollTimerFired(); |
| ExpectTabWasRecentlyAudible(true, last_blurt_time); |
| AdvanceClock(one_polling_interval()); |
| |
| // Poll a silent signal repeatedly, ensuring that the indicator is being |
| // held on during the holding period. |
| SetStreamPower(kStreamId, media::AudioPowerMonitor::zero_power()); |
| for (int i = 0; i < num_silence_polls; ++i) { |
| SimulatePollTimerFired(); |
| ExpectTabWasRecentlyAudible(true, last_blurt_time); |
| // Note: Redundant off timer firings should not have any effect. |
| SimulateOffTimerFired(); |
| ExpectTabWasRecentlyAudible(true, last_blurt_time); |
| AdvanceClock(one_polling_interval()); |
| } |
| |
| ++num_silence_polls; |
| } while (GetTestClockTime() < last_blurt_time + holding_period()); |
| |
| // At this point, the clock has just advanced to beyond the holding period, so |
| // the next firing of the off timer should turn off the tab indicator. Also, |
| // make sure it stays off for several cycles thereafter. |
| ExpectWebContentsWillBeNotifiedOnce(false); |
| for (int i = 0; i < 10; ++i) { |
| SimulateOffTimerFired(); |
| ExpectTabWasRecentlyAudible(false, last_blurt_time); |
| AdvanceClock(one_polling_interval()); |
| } |
| } |
| |
| // Tests that the AudioStreamMonitor correctly processes the blurts from two |
| // different streams in the same tab. |
| TEST_F(AudioStreamMonitorTest, HandlesMultipleStreamsBlurting) { |
| StartMonitoring(kRenderProcessId, kStreamId, CreatePollCallback(kStreamId)); |
| StartMonitoring( |
| kRenderProcessId, kAnotherStreamId, CreatePollCallback(kAnotherStreamId)); |
| |
| base::TimeTicks last_blurt_time; |
| ExpectTabWasRecentlyAudible(false, last_blurt_time); |
| |
| // Measure audible sound from the first stream and silence from the second. |
| // The indicator turns on (i.e., tab was recently audible). |
| ExpectWebContentsWillBeNotifiedOnce(true); |
| SetStreamPower(kStreamId, media::AudioPowerMonitor::max_power()); |
| SetStreamPower(kAnotherStreamId, media::AudioPowerMonitor::zero_power()); |
| last_blurt_time = GetTestClockTime(); |
| SimulatePollTimerFired(); |
| ExpectTabWasRecentlyAudible(true, last_blurt_time); |
| |
| // Halfway through the holding period, the second stream joins in. The |
| // indicator stays on. |
| AdvanceClock(holding_period() / 2); |
| SimulateOffTimerFired(); |
| SetStreamPower(kAnotherStreamId, media::AudioPowerMonitor::max_power()); |
| last_blurt_time = GetTestClockTime(); |
| SimulatePollTimerFired(); // Restarts holding period. |
| ExpectTabWasRecentlyAudible(true, last_blurt_time); |
| |
| // Now, measure silence from both streams. After an entire holding period |
| // has passed (since the second stream joined in), the indicator should turn |
| // off. |
| ExpectWebContentsWillBeNotifiedOnce(false); |
| AdvanceClock(holding_period()); |
| SimulateOffTimerFired(); |
| SetStreamPower(kStreamId, media::AudioPowerMonitor::zero_power()); |
| SetStreamPower(kAnotherStreamId, media::AudioPowerMonitor::zero_power()); |
| SimulatePollTimerFired(); |
| ExpectTabWasRecentlyAudible(false, last_blurt_time); |
| |
| // Now, measure silence from the first stream and audible sound from the |
| // second. The indicator turns back on. |
| ExpectWebContentsWillBeNotifiedOnce(true); |
| SetStreamPower(kAnotherStreamId, media::AudioPowerMonitor::max_power()); |
| last_blurt_time = GetTestClockTime(); |
| SimulatePollTimerFired(); |
| ExpectTabWasRecentlyAudible(true, last_blurt_time); |
| |
| // From here onwards, both streams are silent. Halfway through the holding |
| // period, the indicator should not have changed. |
| SetStreamPower(kAnotherStreamId, media::AudioPowerMonitor::zero_power()); |
| AdvanceClock(holding_period() / 2); |
| SimulatePollTimerFired(); |
| SimulateOffTimerFired(); |
| ExpectTabWasRecentlyAudible(true, last_blurt_time); |
| |
| // Just past the holding period, the indicator should be turned off. |
| ExpectWebContentsWillBeNotifiedOnce(false); |
| AdvanceClock(holding_period() - (GetTestClockTime() - last_blurt_time)); |
| SimulateOffTimerFired(); |
| ExpectTabWasRecentlyAudible(false, last_blurt_time); |
| |
| // Polling should not turn the indicator back while both streams are remaining |
| // silent. |
| for (int i = 0; i < 100; ++i) { |
| AdvanceClock(one_polling_interval()); |
| SimulatePollTimerFired(); |
| ExpectTabWasRecentlyAudible(false, last_blurt_time); |
| } |
| } |
| |
| TEST_F(AudioStreamMonitorTest, MultipleRendererProcesses) { |
| StartMonitoring(kRenderProcessId, kStreamId, CreatePollCallback(kStreamId)); |
| StartMonitoring( |
| kAnotherRenderProcessId, kStreamId, CreatePollCallback(kStreamId)); |
| ExpectIsPolling(kRenderProcessId, kStreamId, true); |
| ExpectIsPolling(kAnotherRenderProcessId, kStreamId, true); |
| StopMonitoring(kAnotherRenderProcessId, kStreamId); |
| ExpectIsPolling(kRenderProcessId, kStreamId, true); |
| ExpectIsPolling(kAnotherRenderProcessId, kStreamId, false); |
| } |
| |
| } // namespace content |