From 9fcad09de8135e931a1d52fab6a6a5741bce59bd Mon Sep 17 00:00:00 2001 From: Chun-Min Chang Date: Mon, 18 Apr 2022 18:45:36 +0000 Subject: [PATCH] Bug 1238038 - Start non-native audio in NonNativeInputTrack r=padenot This patch implements all the functionalities allowing us to use a AudioInputSource to generate audio in NonNativeInputTrack. Depends on D137911 Differential Revision: https://phabricator.services.mozilla.com/D138189 --- dom/media/AudioInputSource.h | 6 +- dom/media/DeviceInputTrack.cpp | 86 ++++++ dom/media/DeviceInputTrack.h | 17 ++ dom/media/gtest/TestAudioTrackGraph.cpp | 278 ++++++++++++++++++ dom/media/gtest/TestDeviceInputTrack.cpp | 355 ++++++++++++++++++----- 5 files changed, 662 insertions(+), 80 deletions(-) diff --git a/dom/media/AudioInputSource.h b/dom/media/AudioInputSource.h index bfa5467efd23..cb1057c65e4f 100644 --- a/dom/media/AudioInputSource.h +++ b/dom/media/AudioInputSource.h @@ -54,11 +54,11 @@ class AudioInputSource : public CubebInputStream::Listener { // The following functions should always be called in the same thread: They // are always run on MediaTrackGraph's graph thread. // Starts producing audio data. - virtual void Start(); + void Start(); // Stops producing audio data. - virtual void Stop(); + void Stop(); // Returns the AudioSegment with aDuration of data inside. - virtual AudioSegment GetAudioSegment(TrackTime aDuration); + AudioSegment GetAudioSegment(TrackTime aDuration); // CubebInputStream::Listener interface: These are used only for the // underlying audio stream. No user should call these APIs. diff --git a/dom/media/DeviceInputTrack.cpp b/dom/media/DeviceInputTrack.cpp index 9fe2e6b289ac..c3e787420160 100644 --- a/dom/media/DeviceInputTrack.cpp +++ b/dom/media/DeviceInputTrack.cpp @@ -402,6 +402,92 @@ void NonNativeInputTrack::NotifyInputStopped(uint32_t aSourceId) { mAudioSource->Stop(); } +AudioInputSourceListener::AudioInputSourceListener(NonNativeInputTrack* aOwner) + : mOwner(aOwner) {} + +void AudioInputSourceListener::AudioDeviceChanged( + AudioInputSource::Id aSourceId) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mOwner); + + if (mOwner->IsDestroyed()) { + LOG("NonNativeInputTrack %p has been destroyed. No need to forward the " + "audio device-changed notification", + mOwner.get()); + return; + } + + class DeviceChangedMessage : public ControlMessage { + public: + DeviceChangedMessage(NonNativeInputTrack* aInputTrack, + AudioInputSource::Id aSourceId) + : ControlMessage(nullptr), + mInputTrack(aInputTrack), + mSourceId(aSourceId) { + MOZ_ASSERT(mInputTrack); + } + void Run() override { + TRACE("NonNativeInputTrack::AudioDeviceChanged ControlMessage"); + mInputTrack->NotifyDeviceChanged(mSourceId); + } + RefPtr mInputTrack; + AudioInputSource::Id mSourceId; + }; + + MOZ_DIAGNOSTIC_ASSERT(mOwner->GraphImpl()); + mOwner->GraphImpl()->AppendMessage( + MakeUnique(mOwner.get(), aSourceId)); +} + +void AudioInputSourceListener::AudioStateCallback( + AudioInputSource::Id aSourceId, + AudioInputSource::EventListener::State aState) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mOwner); + + const char* state = + aState == AudioInputSource::EventListener::State::Started ? "started" + : aState == AudioInputSource::EventListener::State::Stopped ? "stopped" + : aState == AudioInputSource::EventListener::State::Drained ? "drained" + : "error"; + + if (mOwner->IsDestroyed()) { + LOG("NonNativeInputTrack %p has been destroyed. No need to forward the " + "audio state-changed(%s) notification", + mOwner.get(), state); + return; + } + + if (aState == AudioInputSource::EventListener::State::Started) { + LOG("We can ignore %s notification for NonNativeInputTrack %p", state, + mOwner.get()); + return; + } + + LOG("Notify audio stopped due to entering %s state", state); + + class InputStoppedMessage : public ControlMessage { + public: + InputStoppedMessage(NonNativeInputTrack* aInputTrack, + AudioInputSource::Id aSourceId) + : ControlMessage(nullptr), + mInputTrack(aInputTrack), + mSourceId(aSourceId) { + MOZ_ASSERT(mInputTrack); + } + void Run() override { + TRACE("NonNativeInputTrack::AudioStateCallback ControlMessage"); + mInputTrack->NotifyInputStopped(mSourceId); + } + RefPtr mInputTrack; + AudioInputSource::Id mSourceId; + }; + + MOZ_DIAGNOSTIC_ASSERT(mOwner->GraphImpl()); + mOwner->GraphImpl()->AppendMessage( + MakeUnique(mOwner.get(), aSourceId)); +} + #undef LOG_INTERNAL #undef LOG #undef LOGE diff --git a/dom/media/DeviceInputTrack.h b/dom/media/DeviceInputTrack.h index 3114a11c5672..65e2f073dea8 100644 --- a/dom/media/DeviceInputTrack.h +++ b/dom/media/DeviceInputTrack.h @@ -165,6 +165,23 @@ class NonNativeInputTrack final : public DeviceInputTrack { RefPtr mAudioSource; }; +class AudioInputSourceListener : public AudioInputSource::EventListener { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AudioInputSourceListener, override); + + explicit AudioInputSourceListener(NonNativeInputTrack* aOwner); + + // Main thread APIs: + void AudioDeviceChanged(AudioInputSource::Id aSourceId) override; + void AudioStateCallback( + AudioInputSource::Id aSourceId, + AudioInputSource::EventListener::State aState) override; + + private: + ~AudioInputSourceListener() = default; + const RefPtr mOwner; +}; + } // namespace mozilla #endif // DOM_MEDIA_DEVICEINPUTTRACK_H_ diff --git a/dom/media/gtest/TestAudioTrackGraph.cpp b/dom/media/gtest/TestAudioTrackGraph.cpp index d95fb9d100e3..a066d0fc66b5 100644 --- a/dom/media/gtest/TestAudioTrackGraph.cpp +++ b/dom/media/gtest/TestAudioTrackGraph.cpp @@ -10,12 +10,14 @@ #include "gtest/gtest.h" #include "CrossGraphPort.h" +#include "DeviceInputTrack.h" #ifdef MOZ_WEBRTC # include "MediaEngineWebRTCAudio.h" #endif // MOZ_WEBRTC #include "MockCubeb.h" #include "mozilla/Preferences.h" #include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/StaticPrefs_media.h" #include "WaitFor.h" #include "WavDumper.h" @@ -102,6 +104,26 @@ class GoFaster : public ControlMessage { void Run() override { mCubeb->GoFaster(); } }; +struct StartNonNativeInput : public ControlMessage { + const RefPtr mInputTrack; + RefPtr mInputSource; + + StartNonNativeInput(NonNativeInputTrack* aInputTrack, + RefPtr&& aInputSource) + : ControlMessage(aInputTrack), + mInputTrack(aInputTrack), + mInputSource(std::move(aInputSource)) {} + void Run() override { mInputTrack->StartAudio(std::move(mInputSource)); } +}; + +struct StopNonNativeInput : public ControlMessage { + const RefPtr mInputTrack; + + explicit StopNonNativeInput(NonNativeInputTrack* aInputTrack) + : ControlMessage(aInputTrack), mInputTrack(aInputTrack) {} + void Run() override { mInputTrack->StopAudio(); } +}; + } // namespace /* @@ -229,6 +251,262 @@ TEST(TestAudioTrackGraph, NotifyDeviceStarted) WaitFor(cubeb->StreamDestroyEvent()); } +TEST(TestAudioTrackGraph, NonNativeInputTrackStartAndStop) +{ + MockCubeb* cubeb = new MockCubeb(); + CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); + + MediaTrackGraph* graph = MediaTrackGraphImpl::GetInstance( + MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*Window ID*/ 1, + MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr, + GetMainThreadSerialEventTarget()); + + const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1; + + // Add a NonNativeInputTrack to graph, making graph create an output-only + // AudioCallbackDriver since NonNativeInputTrack is an audio-type MediaTrack. + RefPtr track; + auto started = Invoke([&] { + track = new NonNativeInputTrack(graph->GraphRate(), deviceId, + PRINCIPAL_HANDLE_NONE); + graph->AddTrack(track); + return graph->NotifyWhenDeviceStarted(track); + }); + + RefPtr driverStream = WaitFor(cubeb->StreamInitEvent()); + Result rv = WaitFor(started); + EXPECT_TRUE(rv.unwrapOr(false)); + EXPECT_FALSE(driverStream->mHasInput); + EXPECT_TRUE(driverStream->mHasOutput); + + // Main test below: + { + const AudioInputSource::Id sourceId = 1; + const uint32_t channels = 2; + const TrackRate rate = 48000; + const uint32_t bufferingMs = StaticPrefs::media_clockdrift_buffering(); + + // Start and stop the audio in NonNativeInputTrack. + { + struct DeviceInfo { + uint32_t mChannelCount; + AudioInputType mType; + }; + using DeviceQueryPromise = + MozPromise; + + struct DeviceQueryMessage : public ControlMessage { + const NonNativeInputTrack* mInputTrack; + MozPromiseHolder mHolder; + + DeviceQueryMessage(NonNativeInputTrack* aInputTrack, + MozPromiseHolder&& aHolder) + : ControlMessage(aInputTrack), + mInputTrack(aInputTrack), + mHolder(std::move(aHolder)) {} + void Run() override { + DeviceInfo info = {mInputTrack->NumberOfChannels(), + mInputTrack->DevicePreference()}; + // mHolder.Resolve(info, __func__); + mTrack->GraphImpl()->Dispatch(NS_NewRunnableFunction( + "TestAudioTrackGraph::DeviceQueryMessage", + [holder = std::move(mHolder), devInfo = info]() mutable { + holder.Resolve(devInfo, __func__); + })); + } + }; + + // No input channels and device preference before start. + { + MozPromiseHolder h; + RefPtr p = h.Ensure(__func__); + DispatchFunction([&] { + track->GraphImpl()->AppendMessage( + MakeUnique(track.get(), std::move(h))); + }); + Result r = WaitFor(p); + ASSERT_TRUE(r.isOk()); + DeviceInfo info = r.unwrap(); + + EXPECT_EQ(info.mChannelCount, 0U); + EXPECT_EQ(info.mType, AudioInputType::Unknown); + } + + DispatchFunction([&] { + track->GraphImpl()->AppendMessage(MakeUnique( + track.get(), + MakeRefPtr( + MakeRefPtr(track.get()), sourceId, + deviceId, channels, true /* voice */, PRINCIPAL_HANDLE_NONE, + rate, graph->GraphRate(), bufferingMs))); + }); + RefPtr nonNativeStream = + WaitFor(cubeb->StreamInitEvent()); + EXPECT_TRUE(nonNativeStream->mHasInput); + EXPECT_FALSE(nonNativeStream->mHasOutput); + EXPECT_EQ(nonNativeStream->GetInputDeviceID(), deviceId); + EXPECT_EQ(nonNativeStream->InputChannels(), channels); + EXPECT_EQ(nonNativeStream->InputSampleRate(), + static_cast(rate)); + + // Input channels and device preference should be set after start. + { + MozPromiseHolder h; + RefPtr p = h.Ensure(__func__); + DispatchFunction([&] { + track->GraphImpl()->AppendMessage( + MakeUnique(track.get(), std::move(h))); + }); + Result r = WaitFor(p); + ASSERT_TRUE(r.isOk()); + DeviceInfo info = r.unwrap(); + + EXPECT_EQ(info.mChannelCount, channels); + EXPECT_EQ(info.mType, AudioInputType::Voice); + } + + Unused << WaitFor(nonNativeStream->FramesProcessedEvent()); + + DispatchFunction([&] { + track->GraphImpl()->AppendMessage( + MakeUnique(track.get())); + }); + RefPtr destroyedStream = + WaitFor(cubeb->StreamDestroyEvent()); + EXPECT_EQ(destroyedStream.get(), nonNativeStream.get()); + + // No input channels and device preference after stop. + { + MozPromiseHolder h; + RefPtr p = h.Ensure(__func__); + DispatchFunction([&] { + track->GraphImpl()->AppendMessage( + MakeUnique(track.get(), std::move(h))); + }); + Result r = WaitFor(p); + ASSERT_TRUE(r.isOk()); + DeviceInfo info = r.unwrap(); + + EXPECT_EQ(info.mChannelCount, 0U); + EXPECT_EQ(info.mType, AudioInputType::Unknown); + } + } + + // Make sure the NonNativeInputTrack can restart and stop its audio. + { + DispatchFunction([&] { + track->GraphImpl()->AppendMessage(MakeUnique( + track.get(), + MakeRefPtr( + MakeRefPtr(track.get()), sourceId, + deviceId, channels, true, PRINCIPAL_HANDLE_NONE, rate, + graph->GraphRate(), bufferingMs))); + }); + RefPtr nonNativeStream = + WaitFor(cubeb->StreamInitEvent()); + EXPECT_TRUE(nonNativeStream->mHasInput); + EXPECT_FALSE(nonNativeStream->mHasOutput); + EXPECT_EQ(nonNativeStream->GetInputDeviceID(), deviceId); + EXPECT_EQ(nonNativeStream->InputChannels(), channels); + EXPECT_EQ(nonNativeStream->InputSampleRate(), + static_cast(rate)); + + Unused << WaitFor(nonNativeStream->FramesProcessedEvent()); + + DispatchFunction([&] { + track->GraphImpl()->AppendMessage( + MakeUnique(track.get())); + }); + RefPtr destroyedStream = + WaitFor(cubeb->StreamDestroyEvent()); + EXPECT_EQ(destroyedStream.get(), nonNativeStream.get()); + } + } + + // Clean up. + DispatchFunction([&] { track->Destroy(); }); + RefPtr destroyedStream = + WaitFor(cubeb->StreamDestroyEvent()); + EXPECT_EQ(destroyedStream.get(), driverStream.get()); +} + +TEST(TestAudioTrackGraph, NonNativeInputTrackErrorCallback) +{ + MockCubeb* cubeb = new MockCubeb(); + CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); + + MediaTrackGraph* graph = MediaTrackGraphImpl::GetInstance( + MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*Window ID*/ 1, + MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr, + GetMainThreadSerialEventTarget()); + + const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1; + + // Add a NonNativeInputTrack to graph, making graph create an output-only + // AudioCallbackDriver since NonNativeInputTrack is an audio-type MediaTrack. + RefPtr track; + auto started = Invoke([&] { + track = new NonNativeInputTrack(graph->GraphRate(), deviceId, + PRINCIPAL_HANDLE_NONE); + graph->AddTrack(track); + return graph->NotifyWhenDeviceStarted(track); + }); + + RefPtr driverStream = WaitFor(cubeb->StreamInitEvent()); + Result rv = WaitFor(started); + EXPECT_TRUE(rv.unwrapOr(false)); + EXPECT_FALSE(driverStream->mHasInput); + EXPECT_TRUE(driverStream->mHasOutput); + + // Main test below: + { + const AudioInputSource::Id sourceId = 1; + const uint32_t channels = 2; + const TrackRate rate = 48000; + const uint32_t bufferingMs = StaticPrefs::media_clockdrift_buffering(); + + // Launch and start the non-native audio stream. + DispatchFunction([&] { + track->GraphImpl()->AppendMessage(MakeUnique( + track.get(), + MakeRefPtr( + MakeRefPtr(track.get()), sourceId, + deviceId, channels, true, PRINCIPAL_HANDLE_NONE, rate, + graph->GraphRate(), bufferingMs))); + }); + RefPtr nonNativeStream = + WaitFor(cubeb->StreamInitEvent()); + EXPECT_TRUE(nonNativeStream->mHasInput); + EXPECT_FALSE(nonNativeStream->mHasOutput); + EXPECT_EQ(nonNativeStream->GetInputDeviceID(), deviceId); + EXPECT_EQ(nonNativeStream->InputChannels(), channels); + EXPECT_EQ(nonNativeStream->InputSampleRate(), static_cast(rate)); + + // Make sure the audio stream is running. + Unused << WaitFor(nonNativeStream->FramesProcessedEvent()); + + // Force an error. This results in the audio stream destroying. + DispatchFunction([&] { nonNativeStream->ForceError(); }); + WaitFor(nonNativeStream->ErrorForcedEvent()); + + RefPtr destroyedStream = + WaitFor(cubeb->StreamDestroyEvent()); + EXPECT_EQ(destroyedStream.get(), nonNativeStream.get()); + } + + // Make sure it's ok to call audio stop again. + DispatchFunction([&] { + track->GraphImpl()->AppendMessage( + MakeUnique(track.get())); + }); + + // Clean up. + DispatchFunction([&] { track->Destroy(); }); + RefPtr destroyedStream = + WaitFor(cubeb->StreamDestroyEvent()); + EXPECT_EQ(destroyedStream.get(), driverStream.get()); +} + #ifdef MOZ_WEBRTC TEST(TestAudioTrackGraph, ErrorCallback) { diff --git a/dom/media/gtest/TestDeviceInputTrack.cpp b/dom/media/gtest/TestDeviceInputTrack.cpp index c2f30e7d434b..5531c7913470 100644 --- a/dom/media/gtest/TestDeviceInputTrack.cpp +++ b/dom/media/gtest/TestDeviceInputTrack.cpp @@ -11,12 +11,20 @@ #include "AudioGenerator.h" #include "MediaTrackGraphImpl.h" +#include "MockCubeb.h" +#include "WaitFor.h" +#include "mozilla/StaticPrefs_media.h" #include "nsContentUtils.h" using namespace mozilla; using testing::NiceMock; using testing::Return; +namespace { +#define DispatchFunction(f) \ + NS_DispatchToCurrentThread(NS_NewRunnableFunction(__func__, f)) +} // namespace + class MockGraphImpl : public MediaTrackGraphImpl { public: MockGraphImpl(TrackRate aRate, uint32_t aChannels) @@ -150,25 +158,140 @@ TEST_F(TestDeviceInputTrack, OpenTwiceWithoutCloseFirst) { NativeInputTrack::CloseAudio(std::move(track1), nullptr); } -// TODO: Use AudioInputSource with MockCubeb for the test +class MockEventListener : public AudioInputSource::EventListener { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MockEventListener, override); + MOCK_METHOD1(AudioDeviceChanged, void(AudioInputSource::Id)); + MOCK_METHOD2(AudioStateCallback, + void(AudioInputSource::Id, + AudioInputSource::EventListener::State)); + + private: + ~MockEventListener() = default; +}; + +TEST_F(TestDeviceInputTrack, StartAndStop) { + MockCubeb* cubeb = new MockCubeb(); + CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); + + // Non native input settings + const AudioInputSource::Id sourceId = 1; + const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1; + const uint32_t channels = 2; + const PrincipalHandle testPrincipal = + MakePrincipalHandle(nsContentUtils::GetSystemPrincipal()); + const TrackRate rate = 48000; + const uint32_t bufferingMs = StaticPrefs::media_clockdrift_buffering(); + + // Setup: Create a NonNativeInputTrack and add it to mGraph. + RefPtr track = + new NonNativeInputTrack(mGraph->GraphRate(), deviceId, testPrincipal); + mGraph->AddTrack(track); + + // Main test below: + + // Make sure the NonNativeInputTrack can start and stop its audio correctly. + { + auto listener = MakeRefPtr(); + EXPECT_CALL(*listener, + AudioStateCallback( + sourceId, AudioInputSource::EventListener::State::Started)); + EXPECT_CALL(*listener, + AudioStateCallback( + sourceId, AudioInputSource::EventListener::State::Stopped)); + + // No input channels and device preference before start. + EXPECT_EQ(track->NumberOfChannels(), 0U); + EXPECT_EQ(track->DevicePreference(), AudioInputType::Unknown); + + DispatchFunction([&] { + track->StartAudio(MakeRefPtr( + std::move(listener), sourceId, deviceId, channels, true /* voice */, + testPrincipal, rate, mGraph->GraphRate(), bufferingMs)); + }); + + // Wait for stream creation. + RefPtr stream = WaitFor(cubeb->StreamInitEvent()); + + // Make sure the audio stream and the track's settings are correct. + EXPECT_TRUE(stream->mHasInput); + EXPECT_FALSE(stream->mHasOutput); + EXPECT_EQ(stream->GetInputDeviceID(), deviceId); + EXPECT_EQ(stream->InputChannels(), channels); + EXPECT_EQ(stream->InputSampleRate(), static_cast(rate)); + EXPECT_EQ(track->NumberOfChannels(), channels); + EXPECT_EQ(track->DevicePreference(), AudioInputType::Voice); + + // Wait for stream callbacks. + Unused << WaitFor(stream->FramesProcessedEvent()); + + DispatchFunction([&] { track->StopAudio(); }); + + // Wait for stream destroy. + Unused << WaitFor(cubeb->StreamDestroyEvent()); + + // No input channels and device preference after stop. + EXPECT_EQ(track->NumberOfChannels(), 0U); + EXPECT_EQ(track->DevicePreference(), AudioInputType::Unknown); + } + + // Make sure the NonNativeInputTrack can restart its audio correctly. + { + auto listener = MakeRefPtr(); + EXPECT_CALL(*listener, + AudioStateCallback( + sourceId, AudioInputSource::EventListener::State::Started)); + EXPECT_CALL(*listener, + AudioStateCallback( + sourceId, AudioInputSource::EventListener::State::Stopped)); + + DispatchFunction([&] { + track->StartAudio(MakeRefPtr( + std::move(listener), sourceId, deviceId, channels, true, + testPrincipal, rate, mGraph->GraphRate(), bufferingMs)); + }); + + // Wait for stream creation. + RefPtr stream = WaitFor(cubeb->StreamInitEvent()); + EXPECT_TRUE(stream->mHasInput); + EXPECT_FALSE(stream->mHasOutput); + EXPECT_EQ(stream->GetInputDeviceID(), deviceId); + EXPECT_EQ(stream->InputChannels(), channels); + EXPECT_EQ(stream->InputSampleRate(), static_cast(rate)); + + // Wait for stream callbacks. + Unused << WaitFor(stream->FramesProcessedEvent()); + + DispatchFunction([&] { track->StopAudio(); }); + + // Wait for stream destroy. + Unused << WaitFor(cubeb->StreamDestroyEvent()); + } + + // Tear down: Destroy the NativeInputTrack and remove it from mGraph. + track->Destroy(); + mGraph->RemoveTrackGraphThread(track); +} + TEST_F(TestDeviceInputTrack, NonNativeInputTrackData) { + MockCubeb* cubeb = new MockCubeb(); + CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); + // Graph settings const uint32_t flags = 0; const GraphTime frames = 440; // Non native input settings - const uint32_t channelCount = 2; - const TrackRate rate = 48000; - - // Declare Buffer here to make sure it lives longer than the TestAudioSource - // we are going to use below. - AudioSegment buffer; - - const CubebUtils::AudioDeviceID deviceId = (void*)1; + const AudioInputSource::Id sourceId = 1; + const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1; + const uint32_t channels = 2; const PrincipalHandle testPrincipal = MakePrincipalHandle(nsContentUtils::GetSystemPrincipal()); + const TrackRate rate = 48000; + const uint32_t bufferingMs = + static_cast(StaticPrefs::media_clockdrift_buffering()); - // Setup: Create a NonNativeInputTrack and add it to mGraph + // Setup: Create a NonNativeInputTrack and add it to mGraph. RefPtr track = new NonNativeInputTrack(mGraph->GraphRate(), deviceId, testPrincipal); mGraph->AddTrack(track); @@ -188,82 +311,43 @@ TEST_F(TestDeviceInputTrack, NonNativeInputTrackData) { } // Make sure we get the AudioInputSource's data once we start the track. + current = next; next = MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(2 * frames); ASSERT_NE(current, next); // Make sure we have data produced in ProcessInput. - class MockEventListener : public AudioInputSource::EventListener { - public: - NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MockEventListener, override); - MOCK_METHOD1(AudioDeviceChanged, void(AudioInputSource::Id)); - MOCK_METHOD2(AudioStateCallback, - void(AudioInputSource::Id, - AudioInputSource::EventListener::State)); + auto listener = MakeRefPtr(); + EXPECT_CALL(*listener, + AudioStateCallback( + sourceId, AudioInputSource::EventListener::State::Started)); + EXPECT_CALL(*listener, + AudioStateCallback( + sourceId, AudioInputSource::EventListener::State::Stopped)); - private: - ~MockEventListener() = default; - }; + DispatchFunction([&] { + track->StartAudio(MakeRefPtr( + std::move(listener), sourceId, deviceId, channels, true, testPrincipal, + rate, mGraph->GraphRate(), bufferingMs)); + }); + RefPtr stream = WaitFor(cubeb->StreamInitEvent()); + EXPECT_TRUE(stream->mHasInput); + EXPECT_FALSE(stream->mHasOutput); + EXPECT_EQ(stream->GetInputDeviceID(), deviceId); + EXPECT_EQ(stream->InputChannels(), channels); + EXPECT_EQ(stream->InputSampleRate(), static_cast(rate)); - class TestAudioSource final : public AudioInputSource { - public: - TestAudioSource(RefPtr&& aListener, Id aSourceId, - CubebUtils::AudioDeviceID aDeviceId, uint32_t aChannelCount, - bool aIsVoice, const PrincipalHandle& aPrincipalHandle, - TrackRate aSourceRate, TrackRate aTargetRate, - uint32_t aBufferMs, AudioSegment& aCurrentData) - : AudioInputSource(std::move(aListener), aSourceId, aDeviceId, - aChannelCount, aIsVoice, aPrincipalHandle, - aSourceRate, aTargetRate, aBufferMs), - mGenerator(mChannelCount, aTargetRate), - mCurrentData(aCurrentData) {} - - void Start() override {} - void Stop() override {} - AudioSegment GetAudioSegment(TrackTime aDuration) override { - mCurrentData.Clear(); - mGenerator.Generate(mCurrentData, static_cast(aDuration)); - AudioSegment data; - data.AppendSegment(&mCurrentData); - return data; - } - const AudioSegment& GetCurrentData() { return mCurrentData; } - - private: - ~TestAudioSource() = default; - - AudioGenerator mGenerator; - AudioSegment& mCurrentData; - }; - - // We need to make sure the buffer lives longer than the TestAudioSource here - // since TestAudioSource will access buffer by reference. - track->StartAudio(MakeRefPtr( - MakeRefPtr(), 1 /* Ignored */, deviceId, channelCount, - true /* Ignored*/, testPrincipal, rate, mGraph->GraphRate(), - 50 /* Ignored */, buffer)); + // Check audio data. + Unused << WaitFor(stream->FramesProcessedEvent()); track->ProcessInput(current, next, flags); { AudioSegment data; data.AppendSlice(*track->GetData(), current, next); - - TrackTime duration = next - current; - EXPECT_EQ(data.GetDuration(), duration); - EXPECT_EQ(data.GetDuration(), buffer.GetDuration()); - - // The data we get here should be same as the data in buffer. We don't - // implement == operator for AudioSegment so we convert the data into - // interleaved one in nsTArray, which implements the == operator, to do the - // comparison. - nsTArray actual; - size_t actualSampleCount = - data.WriteToInterleavedBuffer(actual, channelCount); - nsTArray expected; - size_t expectedSampleCount = - buffer.WriteToInterleavedBuffer(expected, channelCount); - EXPECT_EQ(actualSampleCount, expectedSampleCount); - EXPECT_EQ(actualSampleCount, static_cast(duration) * - static_cast(channelCount)); - EXPECT_EQ(actual, expected); + EXPECT_FALSE(data.IsNull()); + for (AudioSegment::ConstChunkIterator iter(data); !iter.IsEnded(); + iter.Next()) { + EXPECT_EQ(iter->mChannelData.Length(), channels); + EXPECT_EQ(iter->mPrincipalHandle, testPrincipal); + } } // Stop the track and make sure it produces null data again. @@ -271,7 +355,9 @@ TEST_F(TestDeviceInputTrack, NonNativeInputTrackData) { next = MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(3 * frames); ASSERT_NE(current, next); // Make sure we have data produced in ProcessInput. - track->StopAudio(); + DispatchFunction([&] { track->StopAudio(); }); + Unused << WaitFor(cubeb->StreamDestroyEvent()); + track->ProcessInput(current, next, flags); { AudioSegment data; @@ -283,3 +369,118 @@ TEST_F(TestDeviceInputTrack, NonNativeInputTrackData) { track->Destroy(); mGraph->RemoveTrackGraphThread(track); } + +TEST_F(TestDeviceInputTrack, DeviceChangedCallback) { + MockCubeb* cubeb = new MockCubeb(); + CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); + + // Non native input settings + const AudioInputSource::Id sourceId = 1; + const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1; + const uint32_t channels = 2; + const PrincipalHandle testPrincipal = + MakePrincipalHandle(nsContentUtils::GetSystemPrincipal()); + const TrackRate rate = 48000; + const uint32_t bufferingMs = StaticPrefs::media_clockdrift_buffering(); + + // Setup: Create a NonNativeInputTrack and add it to mGraph. + RefPtr track = + new NonNativeInputTrack(mGraph->GraphRate(), deviceId, testPrincipal); + mGraph->AddTrack(track); + + // Main test below: + + auto listener = MakeRefPtr(); + EXPECT_CALL(*listener, AudioDeviceChanged(sourceId)); + EXPECT_CALL(*listener, + AudioStateCallback( + sourceId, AudioInputSource::EventListener::State::Started)); + EXPECT_CALL(*listener, + AudioStateCallback( + sourceId, AudioInputSource::EventListener::State::Stopped)); + + // Launch and start an audio stream. + DispatchFunction([&] { + track->StartAudio(MakeRefPtr( + std::move(listener), sourceId, deviceId, channels, true, testPrincipal, + rate, mGraph->GraphRate(), bufferingMs)); + }); + RefPtr stream = WaitFor(cubeb->StreamInitEvent()); + EXPECT_TRUE(stream->mHasInput); + EXPECT_FALSE(stream->mHasOutput); + EXPECT_EQ(stream->GetInputDeviceID(), deviceId); + EXPECT_EQ(stream->InputChannels(), channels); + EXPECT_EQ(stream->InputSampleRate(), static_cast(rate)); + + // Make sure the stream is running. + Unused << WaitFor(stream->FramesProcessedEvent()); + + // Fire a device-changed callback. + DispatchFunction([&] { stream->ForceDeviceChanged(); }); + WaitFor(stream->DeviceChangeForcedEvent()); + + // Stop and destroy the stream. + DispatchFunction([&] { track->StopAudio(); }); + Unused << WaitFor(cubeb->StreamDestroyEvent()); + + // Tear down: Destroy the NativeInputTrack and remove it from mGraph. + track->Destroy(); + mGraph->RemoveTrackGraphThread(track); +} + +TEST_F(TestDeviceInputTrack, ErrorCallback) { + MockCubeb* cubeb = new MockCubeb(); + CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); + + // Non native input settings + const AudioInputSource::Id sourceId = 1; + const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1; + const uint32_t channels = 2; + const PrincipalHandle testPrincipal = + MakePrincipalHandle(nsContentUtils::GetSystemPrincipal()); + const TrackRate rate = 48000; + const uint32_t bufferingMs = StaticPrefs::media_clockdrift_buffering(); + + // Setup: Create a NonNativeInputTrack and add it to mGraph. + RefPtr track = + new NonNativeInputTrack(mGraph->GraphRate(), deviceId, testPrincipal); + mGraph->AddTrack(track); + + // Main test below: + + auto listener = MakeRefPtr(); + EXPECT_CALL(*listener, + AudioStateCallback( + sourceId, AudioInputSource::EventListener::State::Started)); + EXPECT_CALL(*listener, + AudioStateCallback( + sourceId, AudioInputSource::EventListener::State::Error)); + + // Launch and start an audio stream. + DispatchFunction([&] { + track->StartAudio(MakeRefPtr( + std::move(listener), sourceId, deviceId, channels, true, testPrincipal, + rate, mGraph->GraphRate(), bufferingMs)); + }); + RefPtr stream = WaitFor(cubeb->StreamInitEvent()); + EXPECT_TRUE(stream->mHasInput); + EXPECT_FALSE(stream->mHasOutput); + EXPECT_EQ(stream->GetInputDeviceID(), deviceId); + EXPECT_EQ(stream->InputChannels(), channels); + EXPECT_EQ(stream->InputSampleRate(), static_cast(rate)); + + // Make sure the stream is running. + Unused << WaitFor(stream->FramesProcessedEvent()); + + // Force an error in the MockCubeb. + DispatchFunction([&] { stream->ForceError(); }); + WaitFor(stream->ErrorForcedEvent()); + + // Stop and destroy the stream. + DispatchFunction([&] { track->StopAudio(); }); + Unused << WaitFor(cubeb->StreamDestroyEvent()); + + // Tear down: Destroy the NativeInputTrack and remove it from mGraph. + track->Destroy(); + mGraph->RemoveTrackGraphThread(track); +}