Chromium Code Reviews| Index: content/browser/renderer_host/media/web_contents_audio_input_stream_unittest.cc |
| diff --git a/content/browser/renderer_host/media/web_contents_audio_input_stream_unittest.cc b/content/browser/renderer_host/media/web_contents_audio_input_stream_unittest.cc |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..8bb1a96989592a10e39c36a43b852f7a5d2f14f3 |
| --- /dev/null |
| +++ b/content/browser/renderer_host/media/web_contents_audio_input_stream_unittest.cc |
| @@ -0,0 +1,490 @@ |
| +// Copyright (c) 2013 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/renderer_host/media/web_contents_audio_input_stream.h" |
| + |
| +#include <list> |
| + |
| +#include "base/bind.h" |
| +#include "base/bind_helpers.h" |
| +#include "base/message_loop.h" |
| +#include "base/synchronization/waitable_event.h" |
| +#include "base/threading/thread.h" |
| +#include "content/browser/browser_thread_impl.h" |
| +#include "content/browser/renderer_host/media/audio_mirroring_manager.h" |
| +#include "content/browser/renderer_host/media/web_contents_capture_util.h" |
| +#include "media/audio/simple_sources.h" |
| +#include "media/audio/virtual_audio_input_stream.h" |
| +#include "testing/gmock/include/gmock/gmock.h" |
| +#include "testing/gtest/include/gtest/gtest.h" |
| + |
| +using ::testing::_; |
| +using ::testing::Assign; |
| +using ::testing::DoAll; |
| +using ::testing::Invoke; |
| +using ::testing::InvokeWithoutArgs; |
| +using ::testing::NotNull; |
| +using ::testing::SaveArg; |
| +using ::testing::WithArgs; |
| + |
| +using media::AudioInputStream; |
| +using media::AudioOutputStream; |
| +using media::AudioParameters; |
| +using media::SineWaveAudioSource; |
| +using media::VirtualAudioInputStream; |
| +using media::VirtualAudioOutputStream; |
| + |
| +namespace content { |
| + |
| +namespace { |
| + |
| +const int kRenderProcessId = 123; |
| +const int kRenderViewId = 456; |
| +const int kAnotherRenderProcessId = 789; |
| +const int kAnotherRenderViewId = 1; |
| + |
| +const AudioParameters& TestAudioParameters() { |
| + static const AudioParameters params( |
| + AudioParameters::AUDIO_FAKE, |
| + media::CHANNEL_LAYOUT_STEREO, |
| + AudioParameters::kAudioCDSampleRate, 16, |
| + AudioParameters::kAudioCDSampleRate / 100); |
| + return params; |
| +} |
| + |
| +class MockAudioMirroringManager : public AudioMirroringManager { |
| + public: |
| + MockAudioMirroringManager() : AudioMirroringManager() {} |
| + virtual ~MockAudioMirroringManager() {} |
| + |
| + MOCK_METHOD3(StartMirroring, |
| + void(int render_process_id, int render_view_id, |
| + MirroringDestination* destination)); |
| + MOCK_METHOD3(StopMirroring, |
| + void(int render_process_id, int render_view_id, |
| + MirroringDestination* destination)); |
| + |
| + private: |
| + DISALLOW_COPY_AND_ASSIGN(MockAudioMirroringManager); |
| +}; |
| + |
| +class MockWebContentsTracker : public WebContentsTracker { |
| + public: |
| + MockWebContentsTracker() : WebContentsTracker() {} |
| + |
| + MOCK_METHOD3(Start, |
| + void(int render_process_id, int render_view_id, |
| + const ChangeCallback& callback)); |
| + MOCK_METHOD0(Stop, void()); |
| + |
| + private: |
| + virtual ~MockWebContentsTracker() {} |
| + |
| + DISALLOW_COPY_AND_ASSIGN(MockWebContentsTracker); |
| +}; |
| + |
| +// A fully-functional VirtualAudioInputStream, but methods are mocked to allow |
| +// tests to check how/when they are invoked. |
| +class MockVirtualAudioInputStream : public VirtualAudioInputStream { |
| + public: |
| + explicit MockVirtualAudioInputStream(base::MessageLoopProxy* message_loop) |
| + : VirtualAudioInputStream(TestAudioParameters(), message_loop), |
| + real_(TestAudioParameters(), message_loop) { |
| + // Set default actions of mocked methods to delegate to the concrete |
| + // implementation. |
| + ON_CALL(*this, Open()) |
| + .WillByDefault(Invoke(&real_, &VirtualAudioInputStream::Open)); |
| + ON_CALL(*this, Start(_)) |
| + .WillByDefault(Invoke(&real_, &VirtualAudioInputStream::Start)); |
| + ON_CALL(*this, Stop()) |
| + .WillByDefault(Invoke(&real_, &VirtualAudioInputStream::Stop)); |
| + ON_CALL(*this, Close()) |
| + .WillByDefault(Invoke(&real_, &VirtualAudioInputStream::Close)); |
| + ON_CALL(*this, GetMaxVolume()) |
| + .WillByDefault(Invoke(&real_, &VirtualAudioInputStream::GetMaxVolume)); |
| + ON_CALL(*this, SetVolume(_)) |
| + .WillByDefault(Invoke(&real_, &VirtualAudioInputStream::SetVolume)); |
| + ON_CALL(*this, GetVolume()) |
| + .WillByDefault(Invoke(&real_, &VirtualAudioInputStream::GetVolume)); |
| + ON_CALL(*this, SetAutomaticGainControl(_)) |
| + .WillByDefault( |
| + Invoke(&real_, &VirtualAudioInputStream::SetAutomaticGainControl)); |
| + ON_CALL(*this, GetAutomaticGainControl()) |
| + .WillByDefault( |
| + Invoke(&real_, &VirtualAudioInputStream::GetAutomaticGainControl)); |
| + ON_CALL(*this, AddOutputStream(NotNull(), _)) |
| + .WillByDefault( |
| + Invoke(&real_, &VirtualAudioInputStream::AddOutputStream)); |
| + ON_CALL(*this, RemoveOutputStream(NotNull(), _)) |
| + .WillByDefault( |
| + Invoke(&real_, &VirtualAudioInputStream::RemoveOutputStream)); |
| + } |
| + |
| + MOCK_METHOD0(Open, bool()); |
| + MOCK_METHOD1(Start, void(AudioInputStream::AudioInputCallback*)); |
| + MOCK_METHOD0(Stop, void()); |
| + MOCK_METHOD0(Close, void()); |
| + MOCK_METHOD0(GetMaxVolume, double()); |
| + MOCK_METHOD1(SetVolume, void(double)); |
| + MOCK_METHOD0(GetVolume, double()); |
| + MOCK_METHOD1(SetAutomaticGainControl, void(bool)); |
| + MOCK_METHOD0(GetAutomaticGainControl, bool()); |
| + MOCK_METHOD2(AddOutputStream, void(VirtualAudioOutputStream*, |
| + const AudioParameters&)); |
| + MOCK_METHOD2(RemoveOutputStream, void(VirtualAudioOutputStream*, |
| + const AudioParameters&)); |
| + |
| + private: |
| + VirtualAudioInputStream real_; |
| + |
| + DISALLOW_COPY_AND_ASSIGN(MockVirtualAudioInputStream); |
| +}; |
| + |
| +class MockAudioInputCallback : public AudioInputStream::AudioInputCallback { |
| + public: |
| + MockAudioInputCallback() {} |
| + |
| + MOCK_METHOD5(OnData, void(AudioInputStream* stream, const uint8* src, |
| + uint32 size, uint32 hardware_delay_bytes, |
| + double volume)); |
| + MOCK_METHOD1(OnClose, void(AudioInputStream* stream)); |
| + MOCK_METHOD2(OnError, void(AudioInputStream* stream, int code)); |
| + |
| + private: |
| + DISALLOW_COPY_AND_ASSIGN(MockAudioInputCallback); |
| +}; |
| + |
| +} // namespace |
| + |
| +class WebContentsAudioInputStreamTest : public testing::Test { |
| + public: |
| + WebContentsAudioInputStreamTest() |
| + : audio_thread_("Audio thread"), |
| + io_thread_(BrowserThread::IO), |
| + mock_mirroring_manager_(new MockAudioMirroringManager()), |
| + mock_tracker_(new MockWebContentsTracker()), |
| + current_render_process_id_(kRenderProcessId), |
| + current_render_view_id_(kRenderViewId), |
| + on_data_event_(false, false) { |
| + audio_thread_.Start(); |
| + io_thread_.Start(); |
| + |
| + mock_vais_ = |
| + new MockVirtualAudioInputStream(audio_thread_.message_loop_proxy()); |
| + wcais_.reset(new WebContentsAudioInputStream( |
| + current_render_process_id_, current_render_view_id_, |
| + audio_thread_.message_loop_proxy(), mock_mirroring_manager_.get(), |
| + mock_tracker_, mock_vais_)); |
| + } |
| + |
| + virtual ~WebContentsAudioInputStreamTest() { |
| + audio_thread_.Stop(); |
| + io_thread_.Stop(); |
| + |
| + ASSERT_TRUE(streams_.empty()); |
| + ASSERT_TRUE(sources_.empty()); |
| + } |
| + |
| + void Open() { |
| + EXPECT_CALL(*mock_vais_, Open()); |
| + EXPECT_CALL(*mock_vais_, Close()); // At Close() time. |
| + |
| + ASSERT_EQ(kRenderProcessId, current_render_process_id_); |
| + ASSERT_EQ(kRenderViewId, current_render_view_id_); |
| + EXPECT_CALL(*mock_tracker_, Start(kRenderProcessId, kRenderViewId, _)) |
| + .WillOnce(DoAll( |
| + SaveArg<2>(&change_callback_), |
| + WithArgs<0, 1>( |
| + Invoke(&change_callback_, |
| + &WebContentsTracker::ChangeCallback::Run)))); |
| + EXPECT_CALL(*mock_tracker_, Stop()); // At Close() time. |
| + |
| + wcais_->Open(); |
| + } |
| + |
| + void Start() { |
| + EXPECT_CALL(*mock_vais_, Start(&mock_input_callback_)); |
| + EXPECT_CALL(*mock_vais_, Stop()); // At Stop() time. |
| + |
| + EXPECT_CALL(*mock_mirroring_manager_, |
| + StartMirroring(kRenderProcessId, kRenderViewId, NotNull())) |
| + .WillOnce(SaveArg<2>(&destination_)) |
| + .RetiresOnSaturation(); |
| + // At Stop() time, or when the mirroring target changes: |
| + EXPECT_CALL(*mock_mirroring_manager_, |
| + StopMirroring(kRenderProcessId, kRenderViewId, NotNull())) |
| + .WillOnce(Assign( |
| + &destination_, |
| + static_cast<AudioMirroringManager::MirroringDestination*>(NULL))) |
| + .RetiresOnSaturation(); |
| + |
| + EXPECT_CALL(mock_input_callback_, OnData(NotNull(), NotNull(), _, _, _)) |
| + .WillRepeatedly( |
| + InvokeWithoutArgs(&on_data_event_, &base::WaitableEvent::Signal)); |
| + EXPECT_CALL(mock_input_callback_, OnClose(_)); // At Stop() time. |
| + |
| + wcais_->Start(&mock_input_callback_); |
| + |
| + // Test plumbing of volume controls and automatic gain controls. Calls to |
| + // wcais_ methods should delegate directly to mock_vais_. |
| + EXPECT_CALL(*mock_vais_, GetVolume()); |
| + double volume = wcais_->GetVolume(); |
| + EXPECT_CALL(*mock_vais_, GetMaxVolume()); |
| + const double max_volume = wcais_->GetMaxVolume(); |
| + volume *= 2.0; |
| + if (volume < max_volume) { |
| + volume = max_volume; |
| + } |
| + EXPECT_CALL(*mock_vais_, SetVolume(volume)); |
| + wcais_->SetVolume(volume); |
| + EXPECT_CALL(*mock_vais_, GetAutomaticGainControl()); |
| + bool auto_gain = wcais_->GetAutomaticGainControl(); |
| + auto_gain = !auto_gain; |
| + EXPECT_CALL(*mock_vais_, SetAutomaticGainControl(auto_gain)); |
| + wcais_->SetAutomaticGainControl(auto_gain); |
| + } |
| + |
| + void AddAnotherInput() { |
| + // Note: WCAIS posts a task to invoke |
| + // MockAudioMirroringManager::StartMirroring() on the IO thread, which |
| + // causes our mock to set |destination_|. Block until that has happened. |
| + base::WaitableEvent done(false, false); |
| + BrowserThread::PostTask( |
| + BrowserThread::IO, FROM_HERE, base::Bind( |
| + &base::WaitableEvent::Signal, base::Unretained(&done))); |
| + done.Wait(); |
| + ASSERT_TRUE(!!destination_); |
| + |
| + EXPECT_CALL(*mock_vais_, AddOutputStream(NotNull(), _)) |
| + .RetiresOnSaturation(); |
| + // Later, when stream is closed: |
| + EXPECT_CALL(*mock_vais_, RemoveOutputStream(NotNull(), _)) |
| + .RetiresOnSaturation(); |
| + |
| + const AudioParameters& params = TestAudioParameters(); |
| + AudioOutputStream* const out = destination_->AddInput(params); |
| + ASSERT_TRUE(!!out); |
|
tommi (sloooow) - chröme
2013/01/14 14:06:48
does this not work?
ASSERT_TRUE(out)
having to no
miu
2013/01/14 21:53:03
Done.
|
| + streams_.push_back(out); |
| + EXPECT_TRUE(out->Open()); |
| + SineWaveAudioSource* const source = new SineWaveAudioSource( |
| + params.channel_layout(), 200.0, params.sample_rate()); |
| + sources_.push_back(source); |
| + out->Start(source); |
| + } |
| + |
| + void RemoveOneInputInFIFOOrder() { |
| + ASSERT_TRUE(!streams_.empty()); |
|
tommi (sloooow) - chröme
2013/01/14 14:06:48
ASSERT_FALSE(streams_.empty()) ?
miu
2013/01/14 21:53:03
Done.
|
| + AudioOutputStream* const out = streams_.front(); |
| + streams_.pop_front(); |
| + out->Stop(); |
| + out->Close(); // Self-deletes. |
| + ASSERT_TRUE(!sources_.empty()); |
| + delete sources_.front(); |
| + sources_.pop_front(); |
| + } |
| + |
| + void ChangeMirroringTarget() { |
| + const int next_render_process_id = |
| + current_render_process_id_ == kRenderProcessId ? |
| + kAnotherRenderProcessId : kRenderProcessId; |
| + const int next_render_view_id = |
| + current_render_view_id_ == kRenderViewId ? |
| + kAnotherRenderViewId : kRenderViewId; |
| + |
| + EXPECT_CALL(*mock_mirroring_manager_, |
| + StartMirroring(next_render_process_id, next_render_view_id, |
| + NotNull())) |
| + .WillOnce(SaveArg<2>(&destination_)) |
| + .RetiresOnSaturation(); |
| + // At Stop() time, or when the mirroring target changes: |
| + EXPECT_CALL(*mock_mirroring_manager_, |
| + StopMirroring(next_render_process_id, next_render_view_id, |
| + NotNull())) |
| + .WillOnce(Assign( |
| + &destination_, |
| + static_cast<AudioMirroringManager::MirroringDestination*>(NULL))) |
| + .RetiresOnSaturation(); |
| + |
| + // Simulate OnTargetChange() callback from WebContentsTracker. |
| + EXPECT_FALSE(change_callback_.is_null()); |
| + change_callback_.Run(next_render_process_id, next_render_view_id); |
| + |
| + current_render_process_id_ = next_render_process_id; |
| + current_render_view_id_ = next_render_view_id; |
| + } |
| + |
| + void LoseMirroringTarget() { |
| + EXPECT_CALL(mock_input_callback_, OnError(_, _)); |
| + |
| + // Simulate OnTargetChange() callback from WebContentsTracker. |
| + EXPECT_FALSE(change_callback_.is_null()); |
| + change_callback_.Run(-1, -1); |
| + } |
| + |
| + void Stop() { |
| + wcais_->Stop(); |
| + } |
| + |
| + void Close() { |
| + // WebContentsAudioInputStream self-destructs on Close(). Its internal |
| + // objects hang around until they are no longer referred to (e.g., as tasks |
| + // on other threads shut things down). |
| + wcais_.release()->Close(); |
| + } |
| + |
| + void RunOnAudioThread(const base::Closure& closure) { |
| + audio_thread_.message_loop()->PostTask(FROM_HERE, closure); |
| + } |
| + |
| + // Block the calling thread until OnData() callbacks are being made. |
| + void WaitForData() { |
| + // Note: Arbitrarily chosen, but more iterations causes tests to take |
| + // significantly more time. |
| + static const int kNumIterations = 3; |
| + for (int i = 0; i < kNumIterations; ++i) { |
|
tommi (sloooow) - chröme
2013/01/14 14:06:48
nit: no need for {}
miu
2013/01/14 21:53:03
Done.
|
| + on_data_event_.Wait(); |
| + } |
| + } |
| + |
| + private: |
| + base::Thread audio_thread_; |
| + BrowserThreadImpl io_thread_; |
| + |
| + scoped_ptr<MockAudioMirroringManager> mock_mirroring_manager_; |
| + scoped_refptr<MockWebContentsTracker> mock_tracker_; |
| + MockVirtualAudioInputStream* mock_vais_; // Owned by wcais_. |
| + scoped_ptr<WebContentsAudioInputStream> wcais_; |
| + |
| + // Mock consumer of audio data. |
| + MockAudioInputCallback mock_input_callback_; |
| + |
| + // Provided by WebContentsAudioInputStream to the mock WebContentsTracker. |
| + // This callback is saved here, and test code will invoke it to simulate |
| + // target change events. |
| + WebContentsTracker::ChangeCallback change_callback_; |
| + |
| + // Provided by WebContentsAudioInputStream to the mock AudioMirroringManager. |
| + // A pointer to the implementation is saved here, and test code will invoke it |
| + // to simulate: 1) calls to AddInput(); and 2) diverting audio data. |
| + AudioMirroringManager::MirroringDestination* destination_; |
| + |
| + // Current target RenderView. These get flipped in ChangedMirroringTarget(). |
| + int current_render_process_id_; |
| + int current_render_view_id_; |
| + |
| + // Streams provided by calls to WebContentsAudioInputStream::AddInput(). Each |
| + // is started with a simulated source of audio data. |
| + std::list<AudioOutputStream*> streams_; |
| + std::list<SineWaveAudioSource*> sources_; // 1:1 with elements in streams_. |
| + |
| + base::WaitableEvent on_data_event_; |
| + |
| + DISALLOW_COPY_AND_ASSIGN(WebContentsAudioInputStreamTest); |
| +}; |
| + |
| +#define RUN_ON_AUDIO_THREAD(method) \ |
| + RunOnAudioThread(base::Bind(&WebContentsAudioInputStreamTest::method, \ |
| + base::Unretained(this))) |
| + |
| +TEST_F(WebContentsAudioInputStreamTest, OpenedButNeverStarted) { |
| + RUN_ON_AUDIO_THREAD(Open); |
| + RUN_ON_AUDIO_THREAD(Close); |
| +} |
| + |
| +TEST_F(WebContentsAudioInputStreamTest, MirroringNothing) { |
| + RUN_ON_AUDIO_THREAD(Open); |
| + RUN_ON_AUDIO_THREAD(Start); |
| + RUN_ON_AUDIO_THREAD(Stop); |
| + RUN_ON_AUDIO_THREAD(Close); |
| +} |
| + |
| +TEST_F(WebContentsAudioInputStreamTest, MirroringOutputOutlivesSession) { |
| + RUN_ON_AUDIO_THREAD(Open); |
| + RUN_ON_AUDIO_THREAD(Start); |
| + RUN_ON_AUDIO_THREAD(AddAnotherInput); |
| + WaitForData(); |
| + RUN_ON_AUDIO_THREAD(Stop); |
| + RUN_ON_AUDIO_THREAD(Close); |
| + RUN_ON_AUDIO_THREAD(RemoveOneInputInFIFOOrder); |
| +} |
| + |
| +TEST_F(WebContentsAudioInputStreamTest, MirroringOutputWithinSession) { |
| + RUN_ON_AUDIO_THREAD(Open); |
| + RUN_ON_AUDIO_THREAD(Start); |
| + RUN_ON_AUDIO_THREAD(AddAnotherInput); |
| + WaitForData(); |
| + RUN_ON_AUDIO_THREAD(RemoveOneInputInFIFOOrder); |
| + RUN_ON_AUDIO_THREAD(Stop); |
| + RUN_ON_AUDIO_THREAD(Close); |
| +} |
| + |
| +TEST_F(WebContentsAudioInputStreamTest, MirroringNothingWithTargetChange) { |
| + RUN_ON_AUDIO_THREAD(Open); |
| + RUN_ON_AUDIO_THREAD(Start); |
| + RUN_ON_AUDIO_THREAD(ChangeMirroringTarget); |
| + RUN_ON_AUDIO_THREAD(Stop); |
| + RUN_ON_AUDIO_THREAD(Close); |
| +} |
| + |
| +TEST_F(WebContentsAudioInputStreamTest, MirroringOneStreamAfterTargetChange) { |
| + RUN_ON_AUDIO_THREAD(Open); |
| + RUN_ON_AUDIO_THREAD(Start); |
| + RUN_ON_AUDIO_THREAD(ChangeMirroringTarget); |
| + RUN_ON_AUDIO_THREAD(AddAnotherInput); |
| + WaitForData(); |
| + RUN_ON_AUDIO_THREAD(Stop); |
| + RUN_ON_AUDIO_THREAD(Close); |
| + RUN_ON_AUDIO_THREAD(RemoveOneInputInFIFOOrder); |
| +} |
| + |
| +TEST_F(WebContentsAudioInputStreamTest, MirroringOneStreamWithTargetChange) { |
| + RUN_ON_AUDIO_THREAD(Open); |
| + RUN_ON_AUDIO_THREAD(Start); |
| + RUN_ON_AUDIO_THREAD(AddAnotherInput); |
| + WaitForData(); |
| + RUN_ON_AUDIO_THREAD(ChangeMirroringTarget); |
| + RUN_ON_AUDIO_THREAD(RemoveOneInputInFIFOOrder); |
| + RUN_ON_AUDIO_THREAD(AddAnotherInput); |
| + WaitForData(); |
| + RUN_ON_AUDIO_THREAD(Stop); |
| + RUN_ON_AUDIO_THREAD(Close); |
| + RUN_ON_AUDIO_THREAD(RemoveOneInputInFIFOOrder); |
| +} |
| + |
| +TEST_F(WebContentsAudioInputStreamTest, MirroringLostTarget) { |
| + RUN_ON_AUDIO_THREAD(Open); |
| + RUN_ON_AUDIO_THREAD(Start); |
| + RUN_ON_AUDIO_THREAD(AddAnotherInput); |
| + WaitForData(); |
| + RUN_ON_AUDIO_THREAD(LoseMirroringTarget); |
| + RUN_ON_AUDIO_THREAD(RemoveOneInputInFIFOOrder); |
| + RUN_ON_AUDIO_THREAD(Stop); |
| + RUN_ON_AUDIO_THREAD(Close); |
| +} |
| + |
| +TEST_F(WebContentsAudioInputStreamTest, MirroringMultipleStreamsAndTargets) { |
| + RUN_ON_AUDIO_THREAD(Open); |
| + RUN_ON_AUDIO_THREAD(Start); |
| + RUN_ON_AUDIO_THREAD(AddAnotherInput); |
| + WaitForData(); |
| + RUN_ON_AUDIO_THREAD(AddAnotherInput); |
| + RUN_ON_AUDIO_THREAD(AddAnotherInput); |
| + RUN_ON_AUDIO_THREAD(AddAnotherInput); |
| + WaitForData(); |
| + RUN_ON_AUDIO_THREAD(ChangeMirroringTarget); |
| + RUN_ON_AUDIO_THREAD(RemoveOneInputInFIFOOrder); |
| + WaitForData(); |
| + RUN_ON_AUDIO_THREAD(RemoveOneInputInFIFOOrder); |
| + RUN_ON_AUDIO_THREAD(RemoveOneInputInFIFOOrder); |
| + RUN_ON_AUDIO_THREAD(AddAnotherInput); |
| + WaitForData(); |
| + RUN_ON_AUDIO_THREAD(RemoveOneInputInFIFOOrder); |
| + WaitForData(); |
| + RUN_ON_AUDIO_THREAD(ChangeMirroringTarget); |
| + RUN_ON_AUDIO_THREAD(RemoveOneInputInFIFOOrder); |
| + RUN_ON_AUDIO_THREAD(Stop); |
| + RUN_ON_AUDIO_THREAD(Close); |
| +} |
| + |
| +} // namespace content |