diff options
Diffstat (limited to 'dom/media/gtest')
109 files changed, 17259 insertions, 0 deletions
diff --git a/dom/media/gtest/AudioGenerator.h b/dom/media/gtest/AudioGenerator.h new file mode 100644 index 0000000000..a528fbef08 --- /dev/null +++ b/dom/media/gtest/AudioGenerator.h @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_MEDIA_GTEST_AUDIO_GENERATOR_H_ +#define DOM_MEDIA_GTEST_AUDIO_GENERATOR_H_ + +#include "AudioSegment.h" +#include "prtime.h" +#include "SineWaveGenerator.h" + +namespace mozilla { + +template <typename Sample> +class AudioGenerator { + public: + AudioGenerator(uint32_t aChannels, uint32_t aSampleRate, + uint32_t aFrequency = 1000) + : mChannels(aChannels), + mSampleRate(aSampleRate), + mFrequency(aFrequency), + mGenerator(aSampleRate, aFrequency) {} + + void Generate(mozilla::AudioSegment& aSegment, const uint32_t& aSamples) { + SetInterleaved(false); + CheckedInt<size_t> bufferSize(sizeof(Sample)); + bufferSize *= aSamples; + RefPtr<SharedBuffer> buffer = SharedBuffer::Create(bufferSize); + Sample* dest = static_cast<Sample*>(buffer->Data()); + mGenerator.generate(dest, aSamples); + AutoTArray<const Sample*, 1> channels; + for (uint32_t i = 0; i < mChannels; ++i) { + channels.AppendElement(dest); + } + aSegment.AppendFrames(buffer.forget(), channels, aSamples, + PRINCIPAL_HANDLE_NONE); + } + + void GenerateInterleaved(Sample* aBuffer, const uint32_t& aFrames) { + SetInterleaved(true); + mGenerator.generate(aBuffer, aFrames * mChannels); + } + + void SetInterleaved(bool aInterleaved) { + if (aInterleaved == mInterleaved) { + return; + } + mInterleaved = aInterleaved; + if (mInterleaved) { + TrackTicks offset = Offset(); + mGenerator = + SineWaveGenerator<Sample>(mSampleRate, mFrequency, mChannels); + mGenerator.SetOffset(offset * mChannels); + } else { + TrackTicks offset = Offset(); + mGenerator = SineWaveGenerator<Sample>(mSampleRate, mFrequency); + mGenerator.SetOffset(offset / mChannels); + } + } + + void SetOffset(TrackTicks aFrames) { mGenerator.SetOffset(aFrames); } + + TrackTicks Offset() const { return mGenerator.Offset(); } + + static float Amplitude() { return SineWaveGenerator<Sample>::Amplitude(); } + + const uint32_t mChannels; + const uint32_t mSampleRate; + const uint32_t mFrequency; + + private: + bool mInterleaved = false; + mozilla::SineWaveGenerator<Sample> mGenerator; +}; + +} // namespace mozilla + +#endif // DOM_MEDIA_GTEST_AUDIO_GENERATOR_H_ diff --git a/dom/media/gtest/AudioVerifier.h b/dom/media/gtest/AudioVerifier.h new file mode 100644 index 0000000000..e50c812f63 --- /dev/null +++ b/dom/media/gtest/AudioVerifier.h @@ -0,0 +1,135 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_MEDIA_GTEST_AUDIOVERIFIER_H_ +#define DOM_MEDIA_GTEST_AUDIOVERIFIER_H_ + +#include "AudioGenerator.h" + +namespace mozilla { + +template <typename Sample> +class AudioVerifier { + public: + explicit AudioVerifier(uint32_t aRate, uint32_t aFrequency) + : mRate(aRate), mFrequency(aFrequency) {} + + // Only the mono channel is taken into account. + void AppendData(const AudioSegment& segment) { + for (AudioSegment::ConstChunkIterator iter(segment); !iter.IsEnded(); + iter.Next()) { + const AudioChunk& c = *iter; + if (c.IsNull()) { + for (int i = 0; i < c.GetDuration(); ++i) { + CheckSample(0); + } + } else { + const Sample* buffer = c.ChannelData<Sample>()[0]; + for (int i = 0; i < c.GetDuration(); ++i) { + CheckSample(buffer[i]); + } + } + } + } + + void AppendDataInterleaved(const Sample* aBuffer, uint32_t aFrames, + uint32_t aChannels) { + for (uint32_t i = 0; i < aFrames * aChannels; i += aChannels) { + CheckSample(aBuffer[i]); + } + } + + float EstimatedFreq() const { + if (mTotalFramesSoFar == PreSilenceSamples()) { + return 0; + } + if (mSumPeriodInSamples == 0) { + return 0; + } + if (mZeroCrossCount <= 1) { + return 0; + } + return mRate / + (static_cast<float>(mSumPeriodInSamples) / (mZeroCrossCount - 1)); + } + + // Returns the maximum difference in value between two adjacent samples along + // the sine curve. + Sample MaxMagnitudeDifference() { + return static_cast<Sample>(AudioGenerator<Sample>::Amplitude() * + sin(2 * M_PI * mFrequency / mRate)); + } + + bool PreSilenceEnded() const { + return mTotalFramesSoFar > mPreSilenceSamples; + } + uint64_t PreSilenceSamples() const { return mPreSilenceSamples; } + uint32_t CountDiscontinuities() const { return mDiscontinuitiesCount; } + + private: + void CheckSample(Sample aCurrentSample) { + ++mTotalFramesSoFar; + // Avoid pre-silence + if (!CountPreSilence(aCurrentSample)) { + CountZeroCrossing(aCurrentSample); + CountDiscontinuities(aCurrentSample); + } + + mPrevious = aCurrentSample; + } + + bool CountPreSilence(Sample aCurrentSample) { + if (IsZero(aCurrentSample) && mPreSilenceSamples == mTotalFramesSoFar - 1) { + ++mPreSilenceSamples; + return true; + } + if (IsZero(mPrevious) && aCurrentSample > 0 && + aCurrentSample < 2 * MaxMagnitudeDifference() && + mPreSilenceSamples == mTotalFramesSoFar - 1) { + // Previous zero considered the first sample of the waveform. + --mPreSilenceSamples; + } + return false; + } + + // Positive to negative direction + void CountZeroCrossing(Sample aCurrentSample) { + if (mPrevious > 0 && aCurrentSample <= 0) { + if (mZeroCrossCount++) { + MOZ_ASSERT(mZeroCrossCount > 1); + mSumPeriodInSamples += mTotalFramesSoFar - mLastZeroCrossPosition; + } + mLastZeroCrossPosition = mTotalFramesSoFar; + } + } + + void CountDiscontinuities(Sample aCurrentSample) { + mDiscontinuitiesCount += fabs(fabs(aCurrentSample) - fabs(mPrevious)) > + 3 * MaxMagnitudeDifference(); + } + + bool IsZero(float aValue) { return fabs(aValue) < 1e-8; } + bool IsZero(short aValue) { return aValue == 0; } + + private: + const uint32_t mRate; + const uint32_t mFrequency; + + uint32_t mZeroCrossCount = 0; + uint64_t mLastZeroCrossPosition = 0; + uint64_t mSumPeriodInSamples = 0; + + uint64_t mTotalFramesSoFar = 0; + uint64_t mPreSilenceSamples = 0; + + uint32_t mDiscontinuitiesCount = 0; + // This is needed to connect the previous buffers. + Sample mPrevious = {}; +}; + +} // namespace mozilla + +#endif // DOM_MEDIA_GTEST_AUDIOVERIFIER_H_ diff --git a/dom/media/gtest/Cargo.toml b/dom/media/gtest/Cargo.toml new file mode 100644 index 0000000000..a55f8fb685 --- /dev/null +++ b/dom/media/gtest/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "mp4parse-gtest" +version = "0.1.0" +authors = ["nobody@mozilla.org"] + +[lib] +path = "hello.rs" diff --git a/dom/media/gtest/GMPTestMonitor.h b/dom/media/gtest/GMPTestMonitor.h new file mode 100644 index 0000000000..ae04ee83dd --- /dev/null +++ b/dom/media/gtest/GMPTestMonitor.h @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef __GMPTestMonitor_h__ +#define __GMPTestMonitor_h__ + +#include "nsThreadUtils.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/SpinEventLoopUntil.h" + +class GMPTestMonitor { + public: + GMPTestMonitor() : mFinished(false) {} + + void AwaitFinished() { + MOZ_ASSERT(NS_IsMainThread()); + mozilla::SpinEventLoopUntil([&]() { return mFinished; }); + mFinished = false; + } + + private: + void MarkFinished() { + MOZ_ASSERT(NS_IsMainThread()); + mFinished = true; + } + + public: + void SetFinished() { + mozilla::SchedulerGroup::Dispatch(mozilla::TaskCategory::Other, + mozilla::NewNonOwningRunnableMethod( + "GMPTestMonitor::MarkFinished", this, + &GMPTestMonitor::MarkFinished)); + } + + private: + bool mFinished; +}; + +#endif // __GMPTestMonitor_h__ diff --git a/dom/media/gtest/MockCubeb.cpp b/dom/media/gtest/MockCubeb.cpp new file mode 100644 index 0000000000..3f457a2394 --- /dev/null +++ b/dom/media/gtest/MockCubeb.cpp @@ -0,0 +1,447 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MockCubeb.h" + +#include "gtest/gtest.h" + +namespace mozilla { + +MockCubebStream::MockCubebStream(cubeb* aContext, cubeb_devid aInputDevice, + cubeb_stream_params* aInputStreamParams, + cubeb_devid aOutputDevice, + cubeb_stream_params* aOutputStreamParams, + cubeb_data_callback aDataCallback, + cubeb_state_callback aStateCallback, + void* aUserPtr, SmartMockCubebStream* aSelf, + bool aFrozenStart) + : context(aContext), + mHasInput(aInputStreamParams), + mHasOutput(aOutputStreamParams), + mSelf(aSelf), + mFrozenStartMonitor("MockCubebStream::mFrozenStartMonitor"), + mFrozenStart(aFrozenStart), + mDataCallback(aDataCallback), + mStateCallback(aStateCallback), + mUserPtr(aUserPtr), + mInputDeviceID(aInputDevice), + mOutputDeviceID(aOutputDevice), + mAudioGenerator(NUM_OF_CHANNELS, + aInputStreamParams ? aInputStreamParams->rate + : aOutputStreamParams->rate, + 100 /* aFrequency */), + mAudioVerifier(aInputStreamParams ? aInputStreamParams->rate + : aOutputStreamParams->rate, + 100 /* aFrequency */) { + if (aInputStreamParams) { + mInputParams = *aInputStreamParams; + } + if (aOutputStreamParams) { + mOutputParams = *aOutputStreamParams; + } +} + +MockCubebStream::~MockCubebStream() = default; + +int MockCubebStream::Start() { + mStateCallback(AsCubebStream(), mUserPtr, CUBEB_STATE_STARTED); + mStreamStop = false; + MonitorAutoLock lock(mFrozenStartMonitor); + if (mFrozenStart) { + NS_DispatchBackgroundTask(NS_NewRunnableFunction( + "MockCubebStream::WaitForThawBeforeStart", + [this, self = RefPtr<SmartMockCubebStream>(mSelf)] { + MonitorAutoLock lock(mFrozenStartMonitor); + while (mFrozenStart) { + mFrozenStartMonitor.Wait(); + } + if (!mStreamStop) { + MockCubeb::AsMock(context)->StartStream(mSelf); + } + })); + return CUBEB_OK; + } + MockCubeb::AsMock(context)->StartStream(this); + return CUBEB_OK; +} + +int MockCubebStream::Stop() { + mOutputVerificationEvent.Notify(MakeTuple( + mAudioVerifier.PreSilenceSamples(), mAudioVerifier.EstimatedFreq(), + mAudioVerifier.CountDiscontinuities())); + int rv = MockCubeb::AsMock(context)->StopStream(this); + mStreamStop = true; + if (rv == CUBEB_OK) { + mStateCallback(AsCubebStream(), mUserPtr, CUBEB_STATE_STOPPED); + } + return rv; +} + +cubeb_stream* MockCubebStream::AsCubebStream() { + return reinterpret_cast<cubeb_stream*>(this); +} + +MockCubebStream* MockCubebStream::AsMock(cubeb_stream* aStream) { + return reinterpret_cast<MockCubebStream*>(aStream); +} + +cubeb_devid MockCubebStream::GetInputDeviceID() const { return mInputDeviceID; } + +cubeb_devid MockCubebStream::GetOutputDeviceID() const { + return mOutputDeviceID; +} + +uint32_t MockCubebStream::InputChannels() const { + return mAudioGenerator.mChannels; +} + +uint32_t MockCubebStream::OutputChannels() const { + return mOutputParams.channels; +} + +uint32_t MockCubebStream::InputSampleRate() const { + return mAudioGenerator.mSampleRate; +} + +uint32_t MockCubebStream::InputFrequency() const { + return mAudioGenerator.mFrequency; +} + +nsTArray<AudioDataValue>&& MockCubebStream::TakeRecordedOutput() { + return std::move(mRecordedOutput); +} + +void MockCubebStream::SetDriftFactor(float aDriftFactor) { + mDriftFactor = aDriftFactor; +} + +void MockCubebStream::ForceError() { mForceErrorState = true; } + +void MockCubebStream::Thaw() { + MonitorAutoLock l(mFrozenStartMonitor); + mFrozenStart = false; + mFrozenStartMonitor.Notify(); +} + +void MockCubebStream::SetOutputRecordingEnabled(bool aEnabled) { + mOutputRecordingEnabled = aEnabled; +} + +MediaEventSource<uint32_t>& MockCubebStream::FramesProcessedEvent() { + return mFramesProcessedEvent; +} + +MediaEventSource<uint32_t>& MockCubebStream::FramesVerifiedEvent() { + return mFramesVerifiedEvent; +} + +MediaEventSource<Tuple<uint64_t, float, uint32_t>>& +MockCubebStream::OutputVerificationEvent() { + return mOutputVerificationEvent; +} + +MediaEventSource<void>& MockCubebStream::ErrorForcedEvent() { + return mErrorForcedEvent; +} + +void MockCubebStream::Process10Ms() { + if (mStreamStop) { + return; + } + + uint32_t rate = mHasOutput ? mOutputParams.rate : mInputParams.rate; + const long nrFrames = + static_cast<long>(static_cast<float>(rate * 10) * mDriftFactor) / + PR_MSEC_PER_SEC; + if (mInputParams.rate) { + mAudioGenerator.GenerateInterleaved(mInputBuffer, nrFrames); + } + cubeb_stream* stream = AsCubebStream(); + const long outframes = + mDataCallback(stream, mUserPtr, mHasInput ? mInputBuffer : nullptr, + mHasOutput ? mOutputBuffer : nullptr, nrFrames); + + if (mOutputRecordingEnabled && mHasOutput) { + mRecordedOutput.AppendElements(mOutputBuffer, outframes * OutputChannels()); + } + mAudioVerifier.AppendDataInterleaved(mOutputBuffer, outframes, + NUM_OF_CHANNELS); + + mFramesProcessedEvent.Notify(outframes); + if (mAudioVerifier.PreSilenceEnded()) { + mFramesVerifiedEvent.Notify(outframes); + } + + if (outframes < nrFrames) { + mStateCallback(stream, mUserPtr, CUBEB_STATE_DRAINED); + mStreamStop = true; + return; + } + if (mForceErrorState) { + mForceErrorState = false; + // Let the audio thread (this thread!) run to completion before + // being released, by joining and releasing on main. + NS_DispatchBackgroundTask( + NS_NewRunnableFunction(__func__, [cubeb = MockCubeb::AsMock(context), + this] { cubeb->StopStream(this); })); + mStateCallback(stream, mUserPtr, CUBEB_STATE_ERROR); + mErrorForcedEvent.Notify(); + mStreamStop = true; + return; + } +} + +MockCubeb::MockCubeb() : ops(&mock_ops) {} + +MockCubeb::~MockCubeb() { MOZ_ASSERT(!mFakeAudioThread); }; + +cubeb* MockCubeb::AsCubebContext() { return reinterpret_cast<cubeb*>(this); } + +MockCubeb* MockCubeb::AsMock(cubeb* aContext) { + return reinterpret_cast<MockCubeb*>(aContext); +} + +int MockCubeb::EnumerateDevices(cubeb_device_type aType, + cubeb_device_collection* collection) { +#ifdef ANDROID + EXPECT_TRUE(false) << "This is not to be called on Android."; +#endif + size_t count = 0; + if (aType & CUBEB_DEVICE_TYPE_INPUT) { + count += mInputDevices.Length(); + } + if (aType & CUBEB_DEVICE_TYPE_OUTPUT) { + count += mOutputDevices.Length(); + } + collection->device = new cubeb_device_info[count]; + collection->count = count; + + uint32_t collection_index = 0; + if (aType & CUBEB_DEVICE_TYPE_INPUT) { + for (auto& device : mInputDevices) { + collection->device[collection_index] = device; + collection_index++; + } + } + if (aType & CUBEB_DEVICE_TYPE_OUTPUT) { + for (auto& device : mOutputDevices) { + collection->device[collection_index] = device; + collection_index++; + } + } + + return CUBEB_OK; +} + +int MockCubeb::RegisterDeviceCollectionChangeCallback( + cubeb_device_type aDevType, + cubeb_device_collection_changed_callback aCallback, void* aUserPtr) { + if (!mSupportsDeviceCollectionChangedCallback) { + return CUBEB_ERROR; + } + + if (aDevType & CUBEB_DEVICE_TYPE_INPUT) { + mInputDeviceCollectionChangeCallback = aCallback; + mInputDeviceCollectionChangeUserPtr = aUserPtr; + } + if (aDevType & CUBEB_DEVICE_TYPE_OUTPUT) { + mOutputDeviceCollectionChangeCallback = aCallback; + mOutputDeviceCollectionChangeUserPtr = aUserPtr; + } + + return CUBEB_OK; +} + +void MockCubeb::AddDevice(cubeb_device_info aDevice) { + if (aDevice.type == CUBEB_DEVICE_TYPE_INPUT) { + mInputDevices.AppendElement(aDevice); + } else if (aDevice.type == CUBEB_DEVICE_TYPE_OUTPUT) { + mOutputDevices.AppendElement(aDevice); + } else { + MOZ_CRASH("bad device type when adding a device in mock cubeb backend"); + } + + bool isInput = aDevice.type & CUBEB_DEVICE_TYPE_INPUT; + if (isInput && mInputDeviceCollectionChangeCallback) { + mInputDeviceCollectionChangeCallback(AsCubebContext(), + mInputDeviceCollectionChangeUserPtr); + } + if (!isInput && mOutputDeviceCollectionChangeCallback) { + mOutputDeviceCollectionChangeCallback(AsCubebContext(), + mOutputDeviceCollectionChangeUserPtr); + } +} + +bool MockCubeb::RemoveDevice(cubeb_devid aId) { + bool foundInput = false; + bool foundOutput = false; + mInputDevices.RemoveElementsBy( + [aId, &foundInput](cubeb_device_info& aDeviceInfo) { + bool foundThisTime = aDeviceInfo.devid == aId; + foundInput |= foundThisTime; + return foundThisTime; + }); + mOutputDevices.RemoveElementsBy( + [aId, &foundOutput](cubeb_device_info& aDeviceInfo) { + bool foundThisTime = aDeviceInfo.devid == aId; + foundOutput |= foundThisTime; + return foundThisTime; + }); + + if (foundInput && mInputDeviceCollectionChangeCallback) { + mInputDeviceCollectionChangeCallback(AsCubebContext(), + mInputDeviceCollectionChangeUserPtr); + } + if (foundOutput && mOutputDeviceCollectionChangeCallback) { + mOutputDeviceCollectionChangeCallback(AsCubebContext(), + mOutputDeviceCollectionChangeUserPtr); + } + // If the device removed was a default device, set another device as the + // default, if there are still devices available. + bool foundDefault = false; + for (uint32_t i = 0; i < mInputDevices.Length(); i++) { + foundDefault |= mInputDevices[i].preferred != CUBEB_DEVICE_PREF_NONE; + } + + if (!foundDefault) { + if (!mInputDevices.IsEmpty()) { + mInputDevices[mInputDevices.Length() - 1].preferred = + CUBEB_DEVICE_PREF_ALL; + } + } + + foundDefault = false; + for (uint32_t i = 0; i < mOutputDevices.Length(); i++) { + foundDefault |= mOutputDevices[i].preferred != CUBEB_DEVICE_PREF_NONE; + } + + if (!foundDefault) { + if (!mOutputDevices.IsEmpty()) { + mOutputDevices[mOutputDevices.Length() - 1].preferred = + CUBEB_DEVICE_PREF_ALL; + } + } + + return foundInput | foundOutput; +} + +void MockCubeb::ClearDevices(cubeb_device_type aType) { + mInputDevices.Clear(); + mOutputDevices.Clear(); +} + +void MockCubeb::SetSupportDeviceChangeCallback(bool aSupports) { + mSupportsDeviceCollectionChangedCallback = aSupports; +} + +void MockCubeb::SetStreamStartFreezeEnabled(bool aEnabled) { + mStreamStartFreezeEnabled = aEnabled; +} + +auto MockCubeb::ForceAudioThread() -> RefPtr<ForcedAudioThreadPromise> { + RefPtr<ForcedAudioThreadPromise> p = + mForcedAudioThreadPromise.Ensure(__func__); + mForcedAudioThread = true; + StartStream(nullptr); + return p; +} + +void MockCubeb::UnforceAudioThread() { + mForcedAudioThread = false; + StopStream(nullptr); +} + +int MockCubeb::StreamInit(cubeb* aContext, cubeb_stream** aStream, + cubeb_devid aInputDevice, + cubeb_stream_params* aInputStreamParams, + cubeb_devid aOutputDevice, + cubeb_stream_params* aOutputStreamParams, + cubeb_data_callback aDataCallback, + cubeb_state_callback aStateCallback, void* aUserPtr) { + auto mockStream = MakeRefPtr<SmartMockCubebStream>( + aContext, aInputDevice, aInputStreamParams, aOutputDevice, + aOutputStreamParams, aDataCallback, aStateCallback, aUserPtr, + mStreamStartFreezeEnabled); + *aStream = mockStream->AsCubebStream(); + mStreamInitEvent.Notify(mockStream); + // AddRef the stream to keep it alive. StreamDestroy releases it. + Unused << mockStream.forget().take(); + return CUBEB_OK; +} + +void MockCubeb::StreamDestroy(cubeb_stream* aStream) { + mStreamDestroyEvent.Notify(); + RefPtr<SmartMockCubebStream> mockStream = + dont_AddRef(MockCubebStream::AsMock(aStream)->mSelf); +} + +void MockCubeb::GoFaster() { mFastMode = true; } + +void MockCubeb::DontGoFaster() { mFastMode = false; } + +MediaEventSource<RefPtr<SmartMockCubebStream>>& MockCubeb::StreamInitEvent() { + return mStreamInitEvent; +} + +MediaEventSource<void>& MockCubeb::StreamDestroyEvent() { + return mStreamDestroyEvent; +} + +void MockCubeb::StartStream(MockCubebStream* aStream) { + auto streams = mLiveStreams.Lock(); + MOZ_ASSERT_IF(!aStream, mForcedAudioThread); + // Forcing an audio thread must happen before starting streams + MOZ_ASSERT_IF(!aStream, streams->IsEmpty()); + if (aStream) { + MOZ_ASSERT(!streams->Contains(aStream->mSelf)); + streams->AppendElement(aStream->mSelf); + } + if (!mFakeAudioThread) { + mFakeAudioThread = WrapUnique(new std::thread(ThreadFunction_s, this)); + } +} + +int MockCubeb::StopStream(MockCubebStream* aStream) { + UniquePtr<std::thread> audioThread; + { + auto streams = mLiveStreams.Lock(); + if (aStream) { + if (!streams->Contains(aStream->mSelf)) { + return CUBEB_ERROR; + } + streams->RemoveElement(aStream->mSelf); + } + MOZ_ASSERT(mFakeAudioThread); + if (streams->IsEmpty() && !mForcedAudioThread) { + audioThread = std::move(mFakeAudioThread); + } + } + if (audioThread) { + audioThread->join(); + } + return CUBEB_OK; +} + +void MockCubeb::ThreadFunction() { + if (mForcedAudioThread) { + mForcedAudioThreadPromise.Resolve(MakeRefPtr<AudioThreadAutoUnforcer>(this), + __func__); + } + while (true) { + { + auto streams = mLiveStreams.Lock(); + for (auto& stream : *streams) { + stream->Process10Ms(); + } + if (streams->IsEmpty() && !mForcedAudioThread) { + break; + } + } + std::this_thread::sleep_for( + std::chrono::microseconds(mFastMode ? 0 : 10 * PR_USEC_PER_MSEC)); + } +} + +} // namespace mozilla diff --git a/dom/media/gtest/MockCubeb.h b/dom/media/gtest/MockCubeb.h new file mode 100644 index 0000000000..3cb703a675 --- /dev/null +++ b/dom/media/gtest/MockCubeb.h @@ -0,0 +1,586 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef MOCKCUBEB_H_ +#define MOCKCUBEB_H_ + +#include "AudioDeviceInfo.h" +#include "AudioGenerator.h" +#include "AudioVerifier.h" +#include "MediaEventSource.h" +#include "mozilla/DataMutex.h" +#include "mozilla/ThreadSafeWeakPtr.h" +#include "nsTArray.h" + +#include <thread> +#include <atomic> +#include <chrono> + +namespace mozilla { +const uint32_t NUM_OF_CHANNELS = 2; + +struct cubeb_ops { + int (*init)(cubeb** context, char const* context_name); + char const* (*get_backend_id)(cubeb* context); + int (*get_max_channel_count)(cubeb* context, uint32_t* max_channels); + int (*get_min_latency)(cubeb* context, cubeb_stream_params params, + uint32_t* latency_ms); + int (*get_preferred_sample_rate)(cubeb* context, uint32_t* rate); + int (*enumerate_devices)(cubeb* context, cubeb_device_type type, + cubeb_device_collection* collection); + int (*device_collection_destroy)(cubeb* context, + cubeb_device_collection* collection); + void (*destroy)(cubeb* context); + int (*stream_init)(cubeb* context, cubeb_stream** stream, + char const* stream_name, cubeb_devid input_device, + cubeb_stream_params* input_stream_params, + cubeb_devid output_device, + cubeb_stream_params* output_stream_params, + unsigned int latency, cubeb_data_callback data_callback, + cubeb_state_callback state_callback, void* user_ptr); + void (*stream_destroy)(cubeb_stream* stream); + int (*stream_start)(cubeb_stream* stream); + int (*stream_stop)(cubeb_stream* stream); + int (*stream_reset_default_device)(cubeb_stream* stream); + int (*stream_get_position)(cubeb_stream* stream, uint64_t* position); + int (*stream_get_latency)(cubeb_stream* stream, uint32_t* latency); + int (*stream_get_input_latency)(cubeb_stream* stream, uint32_t* latency); + int (*stream_set_volume)(cubeb_stream* stream, float volumes); + int (*stream_set_name)(cubeb_stream* stream, char const* stream_name); + int (*stream_get_current_device)(cubeb_stream* stream, + cubeb_device** const device); + int (*stream_device_destroy)(cubeb_stream* stream, cubeb_device* device); + int (*stream_register_device_changed_callback)( + cubeb_stream* stream, + cubeb_device_changed_callback device_changed_callback); + int (*register_device_collection_changed)( + cubeb* context, cubeb_device_type devtype, + cubeb_device_collection_changed_callback callback, void* user_ptr); +}; + +// Keep those and the struct definition in sync with cubeb.h and +// cubeb-internal.h +void cubeb_mock_destroy(cubeb* context); +static int cubeb_mock_enumerate_devices(cubeb* context, cubeb_device_type type, + cubeb_device_collection* out); + +static int cubeb_mock_device_collection_destroy( + cubeb* context, cubeb_device_collection* collection); + +static int cubeb_mock_register_device_collection_changed( + cubeb* context, cubeb_device_type devtype, + cubeb_device_collection_changed_callback callback, void* user_ptr); + +static int cubeb_mock_stream_init( + cubeb* context, cubeb_stream** stream, char const* stream_name, + cubeb_devid input_device, cubeb_stream_params* input_stream_params, + cubeb_devid output_device, cubeb_stream_params* output_stream_params, + unsigned int latency, cubeb_data_callback data_callback, + cubeb_state_callback state_callback, void* user_ptr); + +static int cubeb_mock_stream_start(cubeb_stream* stream); + +static int cubeb_mock_stream_stop(cubeb_stream* stream); + +static void cubeb_mock_stream_destroy(cubeb_stream* stream); + +static char const* cubeb_mock_get_backend_id(cubeb* context); + +static int cubeb_mock_stream_set_volume(cubeb_stream* stream, float volume); + +static int cubeb_mock_get_min_latency(cubeb* context, + cubeb_stream_params params, + uint32_t* latency_ms); + +static int cubeb_mock_get_max_channel_count(cubeb* context, + uint32_t* max_channels); + +// Mock cubeb impl, only supports device enumeration for now. +cubeb_ops const mock_ops = { + /*.init =*/NULL, + /*.get_backend_id =*/cubeb_mock_get_backend_id, + /*.get_max_channel_count =*/cubeb_mock_get_max_channel_count, + /*.get_min_latency =*/cubeb_mock_get_min_latency, + /*.get_preferred_sample_rate =*/NULL, + /*.enumerate_devices =*/cubeb_mock_enumerate_devices, + /*.device_collection_destroy =*/cubeb_mock_device_collection_destroy, + /*.destroy =*/cubeb_mock_destroy, + /*.stream_init =*/cubeb_mock_stream_init, + /*.stream_destroy =*/cubeb_mock_stream_destroy, + /*.stream_start =*/cubeb_mock_stream_start, + /*.stream_stop =*/cubeb_mock_stream_stop, + /*.stream_reset_default_device =*/NULL, + /*.stream_get_position =*/NULL, + /*.stream_get_latency =*/NULL, + /*.stream_get_input_latency =*/NULL, + /*.stream_set_volume =*/cubeb_mock_stream_set_volume, + /*.stream_set_name =*/NULL, + /*.stream_get_current_device =*/NULL, + /*.stream_device_destroy =*/NULL, + /*.stream_register_device_changed_callback =*/NULL, + /*.register_device_collection_changed =*/ + + cubeb_mock_register_device_collection_changed}; + +class SmartMockCubebStream; + +// Represents the fake cubeb_stream. The context instance is needed to +// provide access on cubeb_ops struct. +class MockCubebStream { + public: + MockCubebStream(cubeb* aContext, cubeb_devid aInputDevice, + cubeb_stream_params* aInputStreamParams, + cubeb_devid aOutputDevice, + cubeb_stream_params* aOutputStreamParams, + cubeb_data_callback aDataCallback, + cubeb_state_callback aStateCallback, void* aUserPtr, + SmartMockCubebStream* aSelf, bool aFrozenStart); + + ~MockCubebStream(); + + int Start(); + int Stop(); + + cubeb_stream* AsCubebStream(); + static MockCubebStream* AsMock(cubeb_stream* aStream); + + cubeb_devid GetInputDeviceID() const; + cubeb_devid GetOutputDeviceID() const; + + uint32_t InputChannels() const; + uint32_t OutputChannels() const; + uint32_t InputSampleRate() const; + uint32_t InputFrequency() const; + + void SetDriftFactor(float aDriftFactor); + void ForceError(); + void Thaw(); + + // Enable input recording for this driver. This is best called before + // the thread is running, but is safe to call whenever. + void SetOutputRecordingEnabled(bool aEnabled); + // Get the recorded output from this stream. This doesn't copy, and therefore + // only works once. + nsTArray<AudioDataValue>&& TakeRecordedOutput(); + + MediaEventSource<uint32_t>& FramesProcessedEvent(); + MediaEventSource<uint32_t>& FramesVerifiedEvent(); + MediaEventSource<Tuple<uint64_t, float, uint32_t>>& OutputVerificationEvent(); + MediaEventSource<void>& ErrorForcedEvent(); + + void Process10Ms(); + + public: + cubeb* context = nullptr; + + const bool mHasInput; + const bool mHasOutput; + SmartMockCubebStream* const mSelf; + + private: + // Monitor used to block start until mFrozenStart is false. + Monitor mFrozenStartMonitor; + // Whether this stream should wait for an explicit start request before + // starting. Protected by FrozenStartMonitor. + bool mFrozenStart; + // Signal to the audio thread that stream is stopped. + std::atomic_bool mStreamStop{true}; + // Whether or not the output-side of this stream (what is written from the + // callback output buffer) is recorded in an internal buffer. The data is then + // available via `GetRecordedOutput`. + std::atomic_bool mOutputRecordingEnabled{false}; + // The audio buffer used on data callback. + AudioDataValue mOutputBuffer[NUM_OF_CHANNELS * 1920] = {}; + AudioDataValue mInputBuffer[NUM_OF_CHANNELS * 1920] = {}; + // The audio callback + cubeb_data_callback mDataCallback = nullptr; + // The stream state callback + cubeb_state_callback mStateCallback = nullptr; + // Stream's user data + void* mUserPtr = nullptr; + // The stream params + cubeb_stream_params mOutputParams = {}; + cubeb_stream_params mInputParams = {}; + /* Device IDs */ + cubeb_devid mInputDeviceID; + cubeb_devid mOutputDeviceID; + + std::atomic<float> mDriftFactor{1.0}; + std::atomic_bool mFastMode{false}; + std::atomic_bool mForceErrorState{false}; + AudioGenerator<AudioDataValue> mAudioGenerator; + AudioVerifier<AudioDataValue> mAudioVerifier; + + MediaEventProducer<uint32_t> mFramesProcessedEvent; + MediaEventProducer<uint32_t> mFramesVerifiedEvent; + MediaEventProducer<Tuple<uint64_t, float, uint32_t>> mOutputVerificationEvent; + MediaEventProducer<void> mErrorForcedEvent; + // The recorded data, copied from the output_buffer of the callback. + // Interleaved. + nsTArray<AudioDataValue> mRecordedOutput; +}; + +class SmartMockCubebStream + : public MockCubebStream, + public SupportsThreadSafeWeakPtr<SmartMockCubebStream> { + public: + MOZ_DECLARE_THREADSAFEWEAKREFERENCE_TYPENAME(SmartMockCubebStream) + MOZ_DECLARE_REFCOUNTED_TYPENAME(SmartMockCubebStream) + SmartMockCubebStream(cubeb* aContext, cubeb_devid aInputDevice, + cubeb_stream_params* aInputStreamParams, + cubeb_devid aOutputDevice, + cubeb_stream_params* aOutputStreamParams, + cubeb_data_callback aDataCallback, + cubeb_state_callback aStateCallback, void* aUserPtr, + bool aFrozenStart) + : MockCubebStream(aContext, aInputDevice, aInputStreamParams, + aOutputDevice, aOutputStreamParams, aDataCallback, + aStateCallback, aUserPtr, this, aFrozenStart) {} +}; + +// This class has two facets: it is both a fake cubeb backend that is intended +// to be used for testing, and passed to Gecko code that expects a normal +// backend, but is also controllable by the test code to decide what the backend +// should do, depending on what is being tested. +class MockCubeb { + public: + MockCubeb(); + ~MockCubeb(); + // Cubeb backend implementation + // This allows passing this class as a cubeb* instance. + cubeb* AsCubebContext(); + static MockCubeb* AsMock(cubeb* aContext); + // Fill in the collection parameter with all devices of aType. + int EnumerateDevices(cubeb_device_type aType, + cubeb_device_collection* collection); + + // For a given device type, add a callback, called with a user pointer, when + // the device collection for this backend changes (i.e. a device has been + // removed or added). + int RegisterDeviceCollectionChangeCallback( + cubeb_device_type aDevType, + cubeb_device_collection_changed_callback aCallback, void* aUserPtr); + + // Control API + + // Add an input or output device to this backend. This calls the device + // collection invalidation callback if needed. + void AddDevice(cubeb_device_info aDevice); + // Remove a specific input or output device to this backend, returns true if + // a device was removed. This calls the device collection invalidation + // callback if needed. + bool RemoveDevice(cubeb_devid aId); + // Remove all input or output devices from this backend, without calling the + // callback. This is meant to clean up in between tests. + void ClearDevices(cubeb_device_type aType); + + // This allows simulating a backend that does not support setting a device + // collection invalidation callback, to be able to test the fallback path. + void SetSupportDeviceChangeCallback(bool aSupports); + + // Makes MockCubebStreams starting after this point wait for AllowStart(). + // Callers must ensure they get a hold of the stream through StreamInitEvent + // to be able to start them. + void SetStreamStartFreezeEnabled(bool aEnabled); + + // Helper class that automatically unforces a forced audio thread on release. + class AudioThreadAutoUnforcer { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AudioThreadAutoUnforcer) + + public: + explicit AudioThreadAutoUnforcer(MockCubeb* aContext) + : mContext(aContext) {} + + protected: + virtual ~AudioThreadAutoUnforcer() { mContext->UnforceAudioThread(); } + MockCubeb* mContext; + }; + + // Creates the audio thread if one is not available. The audio thread remains + // forced until UnforceAudioThread is called. The returned promise is resolved + // when the audio thread is running. With this, a test can ensure starting + // audio streams is deterministically fast across platforms for more accurate + // results. + using ForcedAudioThreadPromise = + MozPromise<RefPtr<AudioThreadAutoUnforcer>, nsresult, false>; + RefPtr<ForcedAudioThreadPromise> ForceAudioThread(); + + // Allows a forced audio thread to stop. + void UnforceAudioThread(); + + int StreamInit(cubeb* aContext, cubeb_stream** aStream, + cubeb_devid aInputDevice, + cubeb_stream_params* aInputStreamParams, + cubeb_devid aOutputDevice, + cubeb_stream_params* aOutputStreamParams, + cubeb_data_callback aDataCallback, + cubeb_state_callback aStateCallback, void* aUserPtr); + + void StreamDestroy(cubeb_stream* aStream); + + void GoFaster(); + void DontGoFaster(); + + MediaEventSource<RefPtr<SmartMockCubebStream>>& StreamInitEvent(); + MediaEventSource<void>& StreamDestroyEvent(); + + // MockCubeb specific API + void StartStream(MockCubebStream* aStream); + int StopStream(MockCubebStream* aStream); + + // Simulates the audio thread. The thread is created at Start and destroyed + // at Stop. At next StreamStart a new thread is created. + static void ThreadFunction_s(MockCubeb* aContext) { + aContext->ThreadFunction(); + } + + void ThreadFunction(); + + private: + // This needs to have the exact same memory layout as a real cubeb backend. + // It's very important for this `ops` member to be the very first member of + // the class, and to not have any virtual members (to avoid having a + // vtable). + const cubeb_ops* ops; + // The callback to call when the device list has been changed. + cubeb_device_collection_changed_callback + mInputDeviceCollectionChangeCallback = nullptr; + cubeb_device_collection_changed_callback + mOutputDeviceCollectionChangeCallback = nullptr; + // The pointer to pass in the callback. + void* mInputDeviceCollectionChangeUserPtr = nullptr; + void* mOutputDeviceCollectionChangeUserPtr = nullptr; + void* mUserPtr = nullptr; + // Whether or not this backend supports device collection change + // notification via a system callback. If not, Gecko is expected to re-query + // the list every time. + bool mSupportsDeviceCollectionChangedCallback = true; + // Whether new MockCubebStreams should be frozen on start. + Atomic<bool> mStreamStartFreezeEnabled{false}; + // Whether the audio thread is forced, i.e., whether it remains active even + // with no live streams. + Atomic<bool> mForcedAudioThread{false}; + MozPromiseHolder<ForcedAudioThreadPromise> mForcedAudioThreadPromise; + // Our input and output devices. + nsTArray<cubeb_device_info> mInputDevices; + nsTArray<cubeb_device_info> mOutputDevices; + + // The streams that are currently running. + DataMutex<nsTArray<RefPtr<SmartMockCubebStream>>> mLiveStreams{ + "MockCubeb::mLiveStreams"}; + // Thread that simulates the audio thread, shared across MockCubebStreams to + // avoid unintended drift. This is set together with mLiveStreams, under the + // mLiveStreams DataMutex. + UniquePtr<std::thread> mFakeAudioThread; + // Whether to run the fake audio thread in fast mode, not caring about wall + // clock time. false is default and means data is processed every 10ms. When + // true we sleep(0) between iterations instead of 10ms. + std::atomic<bool> mFastMode{false}; + + MediaEventProducer<RefPtr<SmartMockCubebStream>> mStreamInitEvent; + MediaEventProducer<void> mStreamDestroyEvent; +}; + +void cubeb_mock_destroy(cubeb* context) { delete MockCubeb::AsMock(context); } + +int cubeb_mock_enumerate_devices(cubeb* context, cubeb_device_type type, + cubeb_device_collection* out) { + return MockCubeb::AsMock(context)->EnumerateDevices(type, out); +} + +int cubeb_mock_device_collection_destroy(cubeb* context, + cubeb_device_collection* collection) { + delete[] collection->device; + return CUBEB_OK; +} + +int cubeb_mock_register_device_collection_changed( + cubeb* context, cubeb_device_type devtype, + cubeb_device_collection_changed_callback callback, void* user_ptr) { + return MockCubeb::AsMock(context)->RegisterDeviceCollectionChangeCallback( + devtype, callback, user_ptr); +} + +int cubeb_mock_stream_init( + cubeb* context, cubeb_stream** stream, char const* stream_name, + cubeb_devid input_device, cubeb_stream_params* input_stream_params, + cubeb_devid output_device, cubeb_stream_params* output_stream_params, + unsigned int latency, cubeb_data_callback data_callback, + cubeb_state_callback state_callback, void* user_ptr) { + return MockCubeb::AsMock(context)->StreamInit( + context, stream, input_device, input_stream_params, output_device, + output_stream_params, data_callback, state_callback, user_ptr); +} + +int cubeb_mock_stream_start(cubeb_stream* stream) { + return MockCubebStream::AsMock(stream)->Start(); +} + +int cubeb_mock_stream_stop(cubeb_stream* stream) { + return MockCubebStream::AsMock(stream)->Stop(); +} + +void cubeb_mock_stream_destroy(cubeb_stream* stream) { + MockCubebStream* mockStream = MockCubebStream::AsMock(stream); + MockCubeb* mock = MockCubeb::AsMock(mockStream->context); + return mock->StreamDestroy(stream); +} + +static char const* cubeb_mock_get_backend_id(cubeb* context) { +#if defined(XP_MACOSX) + return "audiounit"; +#elif defined(XP_WIN) + return "wasapi"; +#elif defined(ANDROID) + return "opensl"; +#elif defined(__OpenBSD__) + return "sndio"; +#else + return "pulse"; +#endif +} + +static int cubeb_mock_stream_set_volume(cubeb_stream* stream, float volume) { + return CUBEB_OK; +} + +int cubeb_mock_get_min_latency(cubeb* context, cubeb_stream_params params, + uint32_t* latency_ms) { + *latency_ms = 10; + return CUBEB_OK; +} + +int cubeb_mock_get_max_channel_count(cubeb* context, uint32_t* max_channels) { + *max_channels = NUM_OF_CHANNELS; + return CUBEB_OK; +} + +void PrintDevice(cubeb_device_info aInfo) { + printf( + "id: %zu\n" + "device_id: %s\n" + "friendly_name: %s\n" + "group_id: %s\n" + "vendor_name: %s\n" + "type: %d\n" + "state: %d\n" + "preferred: %d\n" + "format: %d\n" + "default_format: %d\n" + "max_channels: %d\n" + "default_rate: %d\n" + "max_rate: %d\n" + "min_rate: %d\n" + "latency_lo: %d\n" + "latency_hi: %d\n", + reinterpret_cast<uintptr_t>(aInfo.devid), aInfo.device_id, + aInfo.friendly_name, aInfo.group_id, aInfo.vendor_name, aInfo.type, + aInfo.state, aInfo.preferred, aInfo.format, aInfo.default_format, + aInfo.max_channels, aInfo.default_rate, aInfo.max_rate, aInfo.min_rate, + aInfo.latency_lo, aInfo.latency_hi); +} + +void PrintDevice(AudioDeviceInfo* aInfo) { + cubeb_devid id; + nsString name; + nsString groupid; + nsString vendor; + uint16_t type; + uint16_t state; + uint16_t preferred; + uint16_t supportedFormat; + uint16_t defaultFormat; + uint32_t maxChannels; + uint32_t defaultRate; + uint32_t maxRate; + uint32_t minRate; + uint32_t maxLatency; + uint32_t minLatency; + + id = aInfo->DeviceID(); + aInfo->GetName(name); + aInfo->GetGroupId(groupid); + aInfo->GetVendor(vendor); + aInfo->GetType(&type); + aInfo->GetState(&state); + aInfo->GetPreferred(&preferred); + aInfo->GetSupportedFormat(&supportedFormat); + aInfo->GetDefaultFormat(&defaultFormat); + aInfo->GetMaxChannels(&maxChannels); + aInfo->GetDefaultRate(&defaultRate); + aInfo->GetMaxRate(&maxRate); + aInfo->GetMinRate(&minRate); + aInfo->GetMinLatency(&minLatency); + aInfo->GetMaxLatency(&maxLatency); + + printf( + "device id: %zu\n" + "friendly_name: %s\n" + "group_id: %s\n" + "vendor_name: %s\n" + "type: %d\n" + "state: %d\n" + "preferred: %d\n" + "format: %d\n" + "default_format: %d\n" + "max_channels: %d\n" + "default_rate: %d\n" + "max_rate: %d\n" + "min_rate: %d\n" + "latency_lo: %d\n" + "latency_hi: %d\n", + reinterpret_cast<uintptr_t>(id), NS_LossyConvertUTF16toASCII(name).get(), + NS_LossyConvertUTF16toASCII(groupid).get(), + NS_LossyConvertUTF16toASCII(vendor).get(), type, state, preferred, + supportedFormat, defaultFormat, maxChannels, defaultRate, maxRate, + minRate, minLatency, maxLatency); +} + +cubeb_device_info DeviceTemplate(cubeb_devid aId, cubeb_device_type aType, + const char* name) { + // A fake input device + cubeb_device_info device; + device.devid = aId; + device.device_id = "nice name"; + device.friendly_name = name; + device.group_id = "the physical device"; + device.vendor_name = "mozilla"; + device.type = aType; + device.state = CUBEB_DEVICE_STATE_ENABLED; + device.preferred = CUBEB_DEVICE_PREF_NONE; + device.format = CUBEB_DEVICE_FMT_F32NE; + device.default_format = CUBEB_DEVICE_FMT_F32NE; + device.max_channels = 2; + device.default_rate = 44100; + device.max_rate = 44100; + device.min_rate = 16000; + device.latency_lo = 256; + device.latency_hi = 1024; + + return device; +} + +cubeb_device_info DeviceTemplate(cubeb_devid aId, cubeb_device_type aType) { + return DeviceTemplate(aId, aType, "nice name"); +} + +void AddDevices(MockCubeb* mock, uint32_t device_count, + cubeb_device_type deviceType) { + mock->ClearDevices(deviceType); + // Add a few input devices (almost all the same but it does not really + // matter as long as they have distinct IDs and only one is the default + // devices) + for (uintptr_t i = 0; i < device_count; i++) { + cubeb_device_info device = + DeviceTemplate(reinterpret_cast<void*>(i + 1), deviceType); + // Make it so that the last device is the default input device. + if (i == device_count - 1) { + device.preferred = CUBEB_DEVICE_PREF_ALL; + } + mock->AddDevice(device); + } +} +} // namespace mozilla + +#endif // MOCKCUBEB_H_ diff --git a/dom/media/gtest/MockMediaResource.cpp b/dom/media/gtest/MockMediaResource.cpp new file mode 100644 index 0000000000..8811af7c0b --- /dev/null +++ b/dom/media/gtest/MockMediaResource.cpp @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MockMediaResource.h" + +#include <sys/types.h> +#include <sys/stat.h> + +namespace mozilla { + +MockMediaResource::MockMediaResource(const char* aFileName) + : mFileHandle(nullptr), mFileName(aFileName) {} + +nsresult MockMediaResource::Open() { + mFileHandle = fopen(mFileName, "rb"); + if (mFileHandle == nullptr) { + printf_stderr("Can't open %s\n", mFileName); + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +MockMediaResource::~MockMediaResource() { + if (mFileHandle != nullptr) { + fclose(mFileHandle); + } +} + +nsresult MockMediaResource::ReadAt(int64_t aOffset, char* aBuffer, + uint32_t aCount, uint32_t* aBytes) { + if (mFileHandle == nullptr) { + return NS_ERROR_FAILURE; + } + + // Make it fail if we're re-entrant + if (mEntry++) { + MOZ_ASSERT(false); + return NS_ERROR_FAILURE; + } + + fseek(mFileHandle, aOffset, SEEK_SET); + *aBytes = fread(aBuffer, 1, aCount, mFileHandle); + + mEntry--; + + return ferror(mFileHandle) ? NS_ERROR_FAILURE : NS_OK; +} + +int64_t MockMediaResource::GetLength() { + if (mFileHandle == nullptr) { + return -1; + } + fseek(mFileHandle, 0, SEEK_END); + return ftell(mFileHandle); +} + +void MockMediaResource::MockClearBufferedRanges() { mRanges.Clear(); } + +void MockMediaResource::MockAddBufferedRange(int64_t aStart, int64_t aEnd) { + mRanges += MediaByteRange(aStart, aEnd); +} + +int64_t MockMediaResource::GetNextCachedData(int64_t aOffset) { + if (!aOffset) { + return mRanges.Length() ? mRanges[0].mStart : -1; + } + for (size_t i = 0; i < mRanges.Length(); i++) { + if (aOffset == mRanges[i].mStart) { + ++i; + return i < mRanges.Length() ? mRanges[i].mStart : -1; + } + } + return -1; +} + +int64_t MockMediaResource::GetCachedDataEnd(int64_t aOffset) { + for (size_t i = 0; i < mRanges.Length(); i++) { + if (aOffset == mRanges[i].mStart) { + return mRanges[i].mEnd; + } + } + return aOffset; +} + +nsresult MockMediaResource::GetCachedRanges(MediaByteRangeSet& aRanges) { + aRanges = mRanges; + return NS_OK; +} + +} // namespace mozilla diff --git a/dom/media/gtest/MockMediaResource.h b/dom/media/gtest/MockMediaResource.h new file mode 100644 index 0000000000..9ec2a884a0 --- /dev/null +++ b/dom/media/gtest/MockMediaResource.h @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MOCK_MEDIA_RESOURCE_H_ +#define MOCK_MEDIA_RESOURCE_H_ + +#include "MediaResource.h" +#include "nsTArray.h" +#include "mozilla/Atomics.h" + +namespace mozilla { + +DDLoggedTypeDeclNameAndBase(MockMediaResource, MediaResource); + +class MockMediaResource : public MediaResource, + public DecoderDoctorLifeLogger<MockMediaResource> { + public: + explicit MockMediaResource(const char* aFileName); + nsresult ReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount, + uint32_t* aBytes) override; + // Data stored in file, caching recommended. + bool ShouldCacheReads() override { return true; } + void Pin() override {} + void Unpin() override {} + int64_t GetLength() override; + int64_t GetNextCachedData(int64_t aOffset) override; + int64_t GetCachedDataEnd(int64_t aOffset) override; + bool IsDataCachedToEndOfResource(int64_t aOffset) override { return false; } + nsresult ReadFromCache(char* aBuffer, int64_t aOffset, + uint32_t aCount) override { + uint32_t bytesRead = 0; + nsresult rv = ReadAt(aOffset, aBuffer, aCount, &bytesRead); + NS_ENSURE_SUCCESS(rv, rv); + return bytesRead == aCount ? NS_OK : NS_ERROR_FAILURE; + } + + nsresult Open(); + nsresult GetCachedRanges(MediaByteRangeSet& aRanges) override; + + void MockClearBufferedRanges(); + void MockAddBufferedRange(int64_t aStart, int64_t aEnd); + + protected: + virtual ~MockMediaResource(); + + private: + FILE* mFileHandle; + const char* mFileName; + MediaByteRangeSet mRanges; + Atomic<int> mEntry; +}; + +} // namespace mozilla + +#endif diff --git a/dom/media/gtest/TestAudioBuffers.cpp b/dom/media/gtest/TestAudioBuffers.cpp new file mode 100644 index 0000000000..2de1e646fb --- /dev/null +++ b/dom/media/gtest/TestAudioBuffers.cpp @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <stdint.h> +#include "AudioBufferUtils.h" +#include "gtest/gtest.h" +#include <vector> + +const uint32_t FRAMES = 256; + +void test_for_number_of_channels(const uint32_t channels) { + const uint32_t samples = channels * FRAMES; + + mozilla::AudioCallbackBufferWrapper<float> mBuffer(channels); + mozilla::SpillBuffer<float, 128> b(channels); + std::vector<float> fromCallback(samples, 0.0); + std::vector<float> other(samples, 1.0); + + // Set the buffer in the wrapper from the callback + mBuffer.SetBuffer(fromCallback.data(), FRAMES); + + // Fill the SpillBuffer with data. + ASSERT_TRUE(b.Fill(other.data(), 15) == 15); + ASSERT_TRUE(b.Fill(other.data(), 17) == 17); + for (uint32_t i = 0; i < 32 * channels; i++) { + other[i] = 0.0; + } + + // Empty it in the AudioCallbackBufferWrapper + ASSERT_TRUE(b.Empty(mBuffer) == 32); + + // Check available return something reasonnable + ASSERT_TRUE(mBuffer.Available() == FRAMES - 32); + + // Fill the buffer with the rest of the data + mBuffer.WriteFrames(other.data() + 32 * channels, FRAMES - 32); + + // Check the buffer is now full + ASSERT_TRUE(mBuffer.Available() == 0); + + for (uint32_t i = 0; i < samples; i++) { + ASSERT_TRUE(fromCallback[i] == 1.0) + << "Difference at " << i << " (" << fromCallback[i] << " != " << 1.0 + << ")\n"; + } + + ASSERT_TRUE(b.Fill(other.data(), FRAMES) == 128); + ASSERT_TRUE(b.Fill(other.data(), FRAMES) == 0); + ASSERT_TRUE(b.Empty(mBuffer) == 0); +} + +TEST(AudioBuffers, Test) +{ + for (uint32_t ch = 1; ch <= 8; ++ch) { + test_for_number_of_channels(ch); + } +} diff --git a/dom/media/gtest/TestAudioCallbackDriver.cpp b/dom/media/gtest/TestAudioCallbackDriver.cpp new file mode 100644 index 0000000000..981d3aca5d --- /dev/null +++ b/dom/media/gtest/TestAudioCallbackDriver.cpp @@ -0,0 +1,224 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CubebUtils.h" +#include "GraphDriver.h" + +#include "gmock/gmock.h" +#include "gtest/gtest-printers.h" +#include "gtest/gtest.h" + +#include "MediaTrackGraphImpl.h" +#include "mozilla/Attributes.h" +#include "mozilla/UniquePtr.h" +#include "nsTArray.h" + +#include "MockCubeb.h" +#include "WaitFor.h" + +using namespace mozilla; +using IterationResult = GraphInterface::IterationResult; +using ::testing::NiceMock; + +class MockGraphInterface : public GraphInterface { + NS_DECL_THREADSAFE_ISUPPORTS + explicit MockGraphInterface(TrackRate aSampleRate) + : mSampleRate(aSampleRate) {} + MOCK_METHOD4(NotifyOutputData, + void(AudioDataValue*, size_t, TrackRate, uint32_t)); + MOCK_METHOD0(NotifyInputStopped, void()); + MOCK_METHOD5(NotifyInputData, void(const AudioDataValue*, size_t, TrackRate, + uint32_t, uint32_t)); + MOCK_METHOD0(DeviceChanged, void()); + /* OneIteration cannot be mocked because IterationResult is non-memmovable and + * cannot be passed as a parameter, which GMock does internally. */ + IterationResult OneIteration(GraphTime aStateComputedTime, GraphTime, + AudioMixer* aMixer) { + GraphDriver* driver = mCurrentDriver; + if (aMixer) { + aMixer->StartMixing(); + aMixer->Mix(nullptr, + driver->AsAudioCallbackDriver()->OutputChannelCount(), + aStateComputedTime - mStateComputedTime, mSampleRate); + aMixer->FinishMixing(); + } + if (aStateComputedTime != mStateComputedTime) { + mFramesIteratedEvent.Notify(aStateComputedTime - mStateComputedTime); + ++mIterationCount; + } + mStateComputedTime = aStateComputedTime; + if (!mKeepProcessing) { + return IterationResult::CreateStop( + NS_NewRunnableFunction(__func__, [] {})); + } + GraphDriver* next = mNextDriver.exchange(nullptr); + if (next) { + return IterationResult::CreateSwitchDriver( + next, NS_NewRunnableFunction(__func__, [] {})); + } + if (mEnsureNextIteration) { + driver->EnsureNextIteration(); + } + return IterationResult::CreateStillProcessing(); + } + void SetEnsureNextIteration(bool aEnsure) { mEnsureNextIteration = aEnsure; } + +#ifdef DEBUG + bool InDriverIteration(const GraphDriver* aDriver) const override { + return aDriver->OnThread(); + } +#endif + + size_t IterationCount() const { return mIterationCount; } + + GraphTime StateComputedTime() const { return mStateComputedTime; } + void SetCurrentDriver(GraphDriver* aDriver) { mCurrentDriver = aDriver; } + + void StopIterating() { mKeepProcessing = false; } + + void SwitchTo(GraphDriver* aDriver) { mNextDriver = aDriver; } + const TrackRate mSampleRate; + + MediaEventSource<uint32_t>& FramesIteratedEvent() { + return mFramesIteratedEvent; + } + + protected: + Atomic<size_t> mIterationCount{0}; + Atomic<GraphTime> mStateComputedTime{0}; + Atomic<GraphDriver*> mCurrentDriver{nullptr}; + Atomic<bool> mEnsureNextIteration{false}; + Atomic<bool> mKeepProcessing{true}; + Atomic<GraphDriver*> mNextDriver{nullptr}; + MediaEventProducer<uint32_t> mFramesIteratedEvent; + virtual ~MockGraphInterface() = default; +}; + +NS_IMPL_ISUPPORTS0(MockGraphInterface) + +TEST(TestAudioCallbackDriver, StartStop) +MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION { + const TrackRate rate = 44100; + MockCubeb* cubeb = new MockCubeb(); + CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); + + RefPtr<AudioCallbackDriver> driver; + auto graph = MakeRefPtr<NiceMock<MockGraphInterface>>(rate); + EXPECT_CALL(*graph, NotifyInputStopped).Times(0); + ON_CALL(*graph, NotifyOutputData) + .WillByDefault([&](AudioDataValue*, size_t, TrackRate, uint32_t) {}); + + driver = MakeRefPtr<AudioCallbackDriver>(graph, nullptr, rate, 2, 0, nullptr, + nullptr, AudioInputType::Unknown); + EXPECT_FALSE(driver->ThreadRunning()) << "Verify thread is not running"; + EXPECT_FALSE(driver->IsStarted()) << "Verify thread is not started"; + + graph->SetCurrentDriver(driver); + driver->Start(); + // Allow some time to "play" audio. + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + EXPECT_TRUE(driver->ThreadRunning()) << "Verify thread is running"; + EXPECT_TRUE(driver->IsStarted()) << "Verify thread is started"; + + // This will block untill all events have been executed. + MOZ_KnownLive(driver)->Shutdown(); + EXPECT_FALSE(driver->ThreadRunning()) << "Verify thread is not running"; + EXPECT_FALSE(driver->IsStarted()) << "Verify thread is not started"; +} + +void TestSlowStart(const TrackRate aRate) MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION { + std::cerr << "TestSlowStart with rate " << aRate << std::endl; + + MockCubeb* cubeb = new MockCubeb(); + cubeb->SetStreamStartFreezeEnabled(true); + auto unforcer = WaitFor(cubeb->ForceAudioThread()).unwrap(); + Unused << unforcer; + CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); + + RefPtr<AudioCallbackDriver> driver; + auto graph = MakeRefPtr<NiceMock<MockGraphInterface>>(aRate); + EXPECT_CALL(*graph, NotifyInputStopped).Times(0); + + Maybe<int64_t> audioStart; + Maybe<uint32_t> alreadyBuffered; + int64_t inputFrameCount = 0; + int64_t outputFrameCount = 0; + int64_t processedFrameCount = 0; + ON_CALL(*graph, NotifyInputData) + .WillByDefault([&](const AudioDataValue*, size_t aFrames, TrackRate, + uint32_t, uint32_t aAlreadyBuffered) { + if (!audioStart) { + audioStart = Some(graph->StateComputedTime()); + alreadyBuffered = Some(aAlreadyBuffered); + } + EXPECT_NEAR(inputFrameCount, + static_cast<int64_t>(graph->StateComputedTime() - + *audioStart + *alreadyBuffered), + WEBAUDIO_BLOCK_SIZE) + << "Input should be behind state time, due to the delayed start. " + "stateComputedTime=" + << graph->StateComputedTime() << ", audioStartTime=" << *audioStart + << ", alreadyBuffered=" << *alreadyBuffered; + inputFrameCount += aFrames; + }); + ON_CALL(*graph, NotifyOutputData) + .WillByDefault([&](AudioDataValue*, size_t aFrames, TrackRate aRate, + uint32_t) { outputFrameCount += aFrames; }); + + driver = MakeRefPtr<AudioCallbackDriver>(graph, nullptr, aRate, 2, 2, nullptr, + (void*)1, AudioInputType::Voice); + EXPECT_FALSE(driver->ThreadRunning()) << "Verify thread is not running"; + EXPECT_FALSE(driver->IsStarted()) << "Verify thread is not started"; + + graph->SetCurrentDriver(driver); + graph->SetEnsureNextIteration(true); + + driver->Start(); + RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent()); + cubeb->SetStreamStartFreezeEnabled(false); + + const int fallbackIterations = 3; + WaitUntil(graph->FramesIteratedEvent(), [&](uint32_t aFrames) { + const GraphTime tenMillis = aRate / 100; + // An iteration is always rounded upwards to the next full block. + const GraphTime tenMillisIteration = + MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(tenMillis); + // The iteration may be smaller because up to an extra block may have been + // processed and buffered. + const GraphTime tenMillisMinIteration = + tenMillisIteration - WEBAUDIO_BLOCK_SIZE; + // An iteration must be at least one audio block. + const GraphTime minIteration = + std::max<GraphTime>(WEBAUDIO_BLOCK_SIZE, tenMillisMinIteration); + EXPECT_GE(aFrames, minIteration) + << "Fallback driver iteration >= 10ms, modulo an audio block"; + EXPECT_LT(aFrames, static_cast<size_t>(aRate)) + << "Fallback driver iteration <1s (sanity)"; + return graph->IterationCount() >= fallbackIterations; + }); + stream->Thaw(); + + // Wait for at least 100ms of audio data. + WaitUntil(stream->FramesProcessedEvent(), [&](uint32_t aFrames) { + processedFrameCount += aFrames; + return processedFrameCount >= aRate / 10; + }); + + // This will block untill all events have been executed. + MOZ_KnownLive(driver)->Shutdown(); + + EXPECT_EQ(inputFrameCount, outputFrameCount); + EXPECT_NEAR(graph->StateComputedTime() - *audioStart, + inputFrameCount + *alreadyBuffered, WEBAUDIO_BLOCK_SIZE) + << "Graph progresses while audio driver runs. stateComputedTime=" + << graph->StateComputedTime() << ", inputFrameCount=" << inputFrameCount; +} + +TEST(TestAudioCallbackDriver, SlowStart) +MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION { + TestSlowStart(1000); // 10ms = 10 <<< 128 samples + TestSlowStart(8000); // 10ms = 80 < 128 samples + TestSlowStart(44100); // 10ms = 441 > 128 samples +} diff --git a/dom/media/gtest/TestAudioCompactor.cpp b/dom/media/gtest/TestAudioCompactor.cpp new file mode 100644 index 0000000000..8c37a98ddf --- /dev/null +++ b/dom/media/gtest/TestAudioCompactor.cpp @@ -0,0 +1,131 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "gtest/gtest.h" +#include "AudioCompactor.h" +#include "nsDeque.h" +#include "nsIMemoryReporter.h" + +using mozilla::AudioCompactor; +using mozilla::AudioData; +using mozilla::AudioDataValue; +using mozilla::MediaQueue; + +class MemoryFunctor : public nsDequeFunctor<AudioData> { + public: + MemoryFunctor() : mSize(0) {} + MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf); + + void operator()(AudioData* aObject) override { + mSize += aObject->SizeOfIncludingThis(MallocSizeOf); + } + + size_t mSize; +}; + +class TestCopy { + public: + TestCopy(uint32_t aFrames, uint32_t aChannels, uint32_t& aCallCount, + uint32_t& aFrameCount) + : mFrames(aFrames), + mChannels(aChannels), + mCallCount(aCallCount), + mFrameCount(aFrameCount) {} + + uint32_t operator()(AudioDataValue* aBuffer, uint32_t aSamples) { + mCallCount += 1; + uint32_t frames = std::min(mFrames - mFrameCount, aSamples / mChannels); + mFrameCount += frames; + return frames; + } + + private: + const uint32_t mFrames; + const uint32_t mChannels; + uint32_t& mCallCount; + uint32_t& mFrameCount; +}; + +static void TestAudioCompactor(size_t aBytes) { + MediaQueue<AudioData> queue; + AudioCompactor compactor(queue); + + uint64_t offset = 0; + uint64_t time = 0; + uint32_t sampleRate = 44000; + uint32_t channels = 2; + uint32_t frames = aBytes / (channels * sizeof(AudioDataValue)); + size_t maxSlop = aBytes / AudioCompactor::MAX_SLOP_DIVISOR; + + uint32_t callCount = 0; + uint32_t frameCount = 0; + + compactor.Push(offset, time, sampleRate, frames, channels, + TestCopy(frames, channels, callCount, frameCount)); + + EXPECT_GT(callCount, 0U) << "copy functor never called"; + EXPECT_EQ(frames, frameCount) << "incorrect number of frames copied"; + + MemoryFunctor memoryFunc; + queue.LockedForEach(memoryFunc); + size_t allocSize = memoryFunc.mSize - (callCount * sizeof(AudioData)); + size_t slop = allocSize - aBytes; + EXPECT_LE(slop, maxSlop) << "allowed too much allocation slop"; +} + +TEST(Media, AudioCompactor_4000) +{ TestAudioCompactor(4000); } + +TEST(Media, AudioCompactor_4096) +{ TestAudioCompactor(4096); } + +TEST(Media, AudioCompactor_5000) +{ TestAudioCompactor(5000); } + +TEST(Media, AudioCompactor_5256) +{ TestAudioCompactor(5256); } + +TEST(Media, AudioCompactor_NativeCopy) +{ + const uint32_t channels = 2; + const size_t srcBytes = 32; + const uint32_t srcSamples = srcBytes / sizeof(AudioDataValue); + const uint32_t srcFrames = srcSamples / channels; + uint8_t src[srcBytes]; + + for (uint32_t i = 0; i < srcBytes; ++i) { + src[i] = i; + } + + AudioCompactor::NativeCopy copy(src, srcBytes, channels); + + const uint32_t dstSamples = srcSamples * 2; + AudioDataValue dst[dstSamples]; + + const AudioDataValue notCopied = 0xffff; + for (uint32_t i = 0; i < dstSamples; ++i) { + dst[i] = notCopied; + } + + const uint32_t copyCount = 8; + uint32_t copiedFrames = 0; + uint32_t nextSample = 0; + for (uint32_t i = 0; i < copyCount; ++i) { + uint32_t copySamples = dstSamples / copyCount; + copiedFrames += copy(dst + nextSample, copySamples); + nextSample += copySamples; + } + + EXPECT_EQ(srcFrames, copiedFrames) << "copy exact number of source frames"; + + // Verify that the only the correct bytes were copied. + for (uint32_t i = 0; i < dstSamples; ++i) { + if (i < srcSamples) { + EXPECT_NE(notCopied, dst[i]) << "should have copied over these bytes"; + } else { + EXPECT_EQ(notCopied, dst[i]) << "should not have copied over these bytes"; + } + } +} diff --git a/dom/media/gtest/TestAudioDeviceEnumerator.cpp b/dom/media/gtest/TestAudioDeviceEnumerator.cpp new file mode 100644 index 0000000000..58ca2a9822 --- /dev/null +++ b/dom/media/gtest/TestAudioDeviceEnumerator.cpp @@ -0,0 +1,256 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#define ENABLE_SET_CUBEB_BACKEND 1 +#include "CubebDeviceEnumerator.h" +#include "gtest/gtest-printers.h" +#include "gtest/gtest.h" +#include "mozilla/Attributes.h" +#include "mozilla/UniquePtr.h" +#include "nsTArray.h" + +#include "MockCubeb.h" + +using namespace mozilla; + +const bool DEBUG_PRINTS = false; + +enum DeviceOperation { ADD, REMOVE }; + +void TestEnumeration(MockCubeb* aMock, uint32_t aExpectedDeviceCount, + DeviceOperation aOperation, cubeb_device_type aType) { + RefPtr<CubebDeviceEnumerator> enumerator = + CubebDeviceEnumerator::GetInstance(); + + nsTArray<RefPtr<AudioDeviceInfo>> devices; + + if (aType == CUBEB_DEVICE_TYPE_INPUT) { + enumerator->EnumerateAudioInputDevices(devices); + } + + if (aType == CUBEB_DEVICE_TYPE_OUTPUT) { + enumerator->EnumerateAudioOutputDevices(devices); + } + + EXPECT_EQ(devices.Length(), aExpectedDeviceCount) + << "Device count is correct when enumerating"; + + if (DEBUG_PRINTS) { + for (uint32_t i = 0; i < devices.Length(); i++) { + printf("=== Before removal\n"); + PrintDevice(devices[i]); + } + } + + if (aOperation == DeviceOperation::REMOVE) { + aMock->RemoveDevice(reinterpret_cast<cubeb_devid>(1)); + } else { + aMock->AddDevice(DeviceTemplate(reinterpret_cast<cubeb_devid>(123), aType)); + } + + if (aType == CUBEB_DEVICE_TYPE_INPUT) { + enumerator->EnumerateAudioInputDevices(devices); + } + + if (aType == CUBEB_DEVICE_TYPE_OUTPUT) { + enumerator->EnumerateAudioOutputDevices(devices); + } + + uint32_t newExpectedDeviceCount = aOperation == DeviceOperation::REMOVE + ? aExpectedDeviceCount - 1 + : aExpectedDeviceCount + 1; + + EXPECT_EQ(devices.Length(), newExpectedDeviceCount) + << "Device count is correct when enumerating after operation"; + + if (DEBUG_PRINTS) { + for (uint32_t i = 0; i < devices.Length(); i++) { + printf("=== After removal\n"); + PrintDevice(devices[i]); + } + } +} + +#ifndef ANDROID +TEST(CubebDeviceEnumerator, EnumerateSimple) +{ + // It looks like we're leaking this object, but in fact it will be freed by + // gecko sometime later: `cubeb_destroy` is called when layout statics are + // shutdown and we cast back to a MockCubeb* and call the dtor. + MockCubeb* mock = new MockCubeb(); + mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext()); + + // We want to test whether CubebDeviceEnumerator works with and without a + // backend that can notify of a device collection change via callback. + // Additionally, we're testing that both adding and removing a device + // invalidates the list correctly. + bool supportsDeviceChangeCallback[2] = {true, false}; + DeviceOperation operations[2] = {DeviceOperation::ADD, + DeviceOperation::REMOVE}; + + for (bool supports : supportsDeviceChangeCallback) { + // Shutdown for `supports` to take effect + CubebDeviceEnumerator::Shutdown(); + mock->SetSupportDeviceChangeCallback(supports); + for (DeviceOperation op : operations) { + uint32_t device_count = 4; + + cubeb_device_type deviceType = CUBEB_DEVICE_TYPE_INPUT; + AddDevices(mock, device_count, deviceType); + TestEnumeration(mock, device_count, op, deviceType); + + deviceType = CUBEB_DEVICE_TYPE_OUTPUT; + AddDevices(mock, device_count, deviceType); + TestEnumeration(mock, device_count, op, deviceType); + } + } + // Shutdown to clean up the last `supports` effect + CubebDeviceEnumerator::Shutdown(); +} + +#else // building for Android, which has no device enumeration support +TEST(CubebDeviceEnumerator, EnumerateAndroid) +{ + MockCubeb* mock = new MockCubeb(); + mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext()); + + RefPtr<CubebDeviceEnumerator> enumerator = + CubebDeviceEnumerator::GetInstance(); + + nsTArray<RefPtr<AudioDeviceInfo>> inputDevices; + enumerator->EnumerateAudioInputDevices(inputDevices); + EXPECT_EQ(inputDevices.Length(), 1u) + << "Android always exposes a single input device."; + EXPECT_EQ(inputDevices[0]->MaxChannels(), 1u) << "With a single channel."; + EXPECT_EQ(inputDevices[0]->DeviceID(), nullptr) + << "It's always the default input device."; + EXPECT_TRUE(inputDevices[0]->Preferred()) + << "it's always the prefered input device."; + + nsTArray<RefPtr<AudioDeviceInfo>> outputDevices; + enumerator->EnumerateAudioOutputDevices(outputDevices); + EXPECT_EQ(outputDevices.Length(), 1u) + << "Android always exposes a single output device."; + EXPECT_EQ(outputDevices[0]->MaxChannels(), 2u) << "With stereo channels."; + EXPECT_EQ(outputDevices[0]->DeviceID(), nullptr) + << "It's always the default output device."; + EXPECT_TRUE(outputDevices[0]->Preferred()) + << "it's always the prefered output device."; +} +#endif + +TEST(CubebDeviceEnumerator, ForceNullCubebContext) +{ + mozilla::CubebUtils::ForceSetCubebContext(nullptr); + RefPtr<CubebDeviceEnumerator> enumerator = + CubebDeviceEnumerator::GetInstance(); + + nsTArray<RefPtr<AudioDeviceInfo>> inputDevices; + enumerator->EnumerateAudioInputDevices(inputDevices); + EXPECT_EQ(inputDevices.Length(), 0u) + << "Enumeration must fail, input device list must be empty."; + + nsTArray<RefPtr<AudioDeviceInfo>> outputDevices; + enumerator->EnumerateAudioOutputDevices(outputDevices); + EXPECT_EQ(outputDevices.Length(), 0u) + << "Enumeration must fail, output device list must be empty."; + + // Shutdown to clean up the null context effect + CubebDeviceEnumerator::Shutdown(); +} + +TEST(CubebDeviceEnumerator, DeviceInfoFromId) +{ + MockCubeb* mock = new MockCubeb(); + mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext()); + + uint32_t device_count = 4; + cubeb_device_type deviceTypes[2] = {CUBEB_DEVICE_TYPE_INPUT, + CUBEB_DEVICE_TYPE_OUTPUT}; + + bool supportsDeviceChangeCallback[2] = {true, false}; + for (bool supports : supportsDeviceChangeCallback) { + // Shutdown for `supports` to take effect + CubebDeviceEnumerator::Shutdown(); + mock->SetSupportDeviceChangeCallback(supports); + for (cubeb_device_type& deviceType : deviceTypes) { + AddDevices(mock, device_count, deviceType); + + cubeb_devid id_1 = reinterpret_cast<cubeb_devid>(1); + RefPtr<CubebDeviceEnumerator> enumerator = + CubebDeviceEnumerator::GetInstance(); + RefPtr<AudioDeviceInfo> devInfo = enumerator->DeviceInfoFromID(id_1); + EXPECT_TRUE(devInfo) << "the device exist"; + EXPECT_EQ(devInfo->DeviceID(), id_1) << "verify the device"; + + mock->RemoveDevice(id_1); + devInfo = enumerator->DeviceInfoFromID(id_1); + EXPECT_FALSE(devInfo) << "the device does not exist any more"; + + cubeb_devid id_5 = reinterpret_cast<cubeb_devid>(5); + mock->AddDevice(DeviceTemplate(id_5, deviceType)); + devInfo = enumerator->DeviceInfoFromID(id_5); + EXPECT_TRUE(devInfo) << "newly added device must exist"; + EXPECT_EQ(devInfo->DeviceID(), id_5) << "verify the device"; + } + } + // Shutdown for `supports` to take effect + CubebDeviceEnumerator::Shutdown(); +} + +TEST(CubebDeviceEnumerator, DeviceInfoFromName) +{ + MockCubeb* mock = new MockCubeb(); + mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext()); + + cubeb_device_type deviceTypes[2] = {CUBEB_DEVICE_TYPE_INPUT, + CUBEB_DEVICE_TYPE_OUTPUT}; + + bool supportsDeviceChangeCallback[2] = {true, false}; + for (bool supports : supportsDeviceChangeCallback) { + // Shutdown for `supports` to take effect + CubebDeviceEnumerator::Shutdown(); + mock->SetSupportDeviceChangeCallback(supports); + for (cubeb_device_type& deviceType : deviceTypes) { + cubeb_devid id_1 = reinterpret_cast<cubeb_devid>(1); + mock->AddDevice(DeviceTemplate(id_1, deviceType, "device name 1")); + cubeb_devid id_2 = reinterpret_cast<cubeb_devid>(2); + nsCString device_name = "device name 2"_ns; + mock->AddDevice(DeviceTemplate(id_2, deviceType, device_name.get())); + cubeb_devid id_3 = reinterpret_cast<cubeb_devid>(3); + mock->AddDevice(DeviceTemplate(id_3, deviceType, "device name 3")); + + RefPtr<CubebDeviceEnumerator> enumerator = + CubebDeviceEnumerator::GetInstance(); + + RefPtr<AudioDeviceInfo> devInfo = + enumerator->DeviceInfoFromName(NS_ConvertUTF8toUTF16(device_name)); + EXPECT_TRUE(devInfo) << "the device exist"; + EXPECT_EQ(devInfo->Name(), NS_ConvertUTF8toUTF16(device_name)) + << "verify the device"; + + EnumeratorSide side = (deviceType == CUBEB_DEVICE_TYPE_INPUT) + ? EnumeratorSide::INPUT + : EnumeratorSide::OUTPUT; + devInfo = enumerator->DeviceInfoFromName( + NS_ConvertUTF8toUTF16(device_name), side); + EXPECT_TRUE(devInfo) << "the device exist"; + EXPECT_EQ(devInfo->Name(), NS_ConvertUTF8toUTF16(device_name)) + << "verify the device"; + + mock->RemoveDevice(id_2); + devInfo = + enumerator->DeviceInfoFromName(NS_ConvertUTF8toUTF16(device_name)); + EXPECT_FALSE(devInfo) << "the device does not exist any more"; + + devInfo = enumerator->DeviceInfoFromName( + NS_ConvertUTF8toUTF16(device_name), side); + EXPECT_FALSE(devInfo) << "the device does not exist any more"; + } + } + // Shutdown for `supports` to take effect + CubebDeviceEnumerator::Shutdown(); +} +#undef ENABLE_SET_CUBEB_BACKEND diff --git a/dom/media/gtest/TestAudioDriftCorrection.cpp b/dom/media/gtest/TestAudioDriftCorrection.cpp new file mode 100644 index 0000000000..576d90f4ab --- /dev/null +++ b/dom/media/gtest/TestAudioDriftCorrection.cpp @@ -0,0 +1,404 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AudioDriftCorrection.h" +#include "AudioGenerator.h" +#include "AudioVerifier.h" + +#include "gmock/gmock.h" +#include "gtest/gtest-printers.h" +#include "gtest/gtest.h" + +using namespace mozilla; + +// Runs UpdateClock() and checks that the reported correction level doesn't +// change for enough time to trigger a correction update on the first +// following UpdateClock(). Returns the first reported correction level. +static float RunUntilCorrectionUpdate(ClockDrift& aC, uint32_t aSource, + uint32_t aTarget, uint32_t aBuffering, + uint32_t aSaturation, + uint32_t aSourceOffset = 0, + uint32_t aTargetOffset = 0) { + Maybe<float> correction; + for (uint32_t s = aSourceOffset, t = aTargetOffset; + s < aC.mSourceRate && t < aC.mTargetRate; s += aSource, t += aTarget) { + aC.UpdateClock(aSource, aTarget, aBuffering, aSaturation); + if (correction) { + EXPECT_FLOAT_EQ(aC.GetCorrection(), *correction) + << "s=" << s << "; t=" << t; + } else { + correction = Some(aC.GetCorrection()); + } + } + return *correction; +}; + +TEST(TestClockDrift, Basic) +{ + // Keep buffered frames to the wanted level in order to not affect that test. + const uint32_t buffered = 5 * 480; + + ClockDrift c(48000, 48000, buffered); + EXPECT_EQ(c.GetCorrection(), 1.0); + + EXPECT_FLOAT_EQ(RunUntilCorrectionUpdate(c, 480, 480, buffered, buffered), + 1.0); + EXPECT_FLOAT_EQ( + RunUntilCorrectionUpdate(c, 480, 480 + 48, buffered, buffered), 1.0); + EXPECT_FLOAT_EQ(RunUntilCorrectionUpdate(c, 480, 480, buffered, buffered), + 1.06); + EXPECT_FLOAT_EQ( + RunUntilCorrectionUpdate(c, 480 + 48, 480, buffered, buffered), 1.024); + + c.UpdateClock(0, 0, 5 * 480, 5 * 480); + EXPECT_FLOAT_EQ(c.GetCorrection(), 0.95505452); +} + +TEST(TestClockDrift, BasicResampler) +{ + // Keep buffered frames to the wanted level in order to not affect that test. + const uint32_t buffered = 5 * 240; + + ClockDrift c(24000, 48000, buffered); + + // Keep buffered frames to the wanted level in order to not affect that test. + EXPECT_FLOAT_EQ(RunUntilCorrectionUpdate(c, 240, 480, buffered, buffered), + 1.0); + + // +10% + EXPECT_FLOAT_EQ( + RunUntilCorrectionUpdate(c, 240, 480 + 48, buffered, buffered), 1.0); + + // +10% + EXPECT_FLOAT_EQ( + RunUntilCorrectionUpdate(c, 240 + 24, 480, buffered, buffered), 1.06); + + // -10% + EXPECT_FLOAT_EQ( + RunUntilCorrectionUpdate(c, 240, 480 - 48, buffered, buffered), + 0.96945453); + + // +5%, -5% + EXPECT_FLOAT_EQ( + RunUntilCorrectionUpdate(c, 240 + 12, 480 - 24, buffered, buffered), + 0.92778182); + + c.UpdateClock(0, 0, buffered, buffered); + EXPECT_FLOAT_EQ(c.GetCorrection(), 0.91396987); +} + +TEST(TestClockDrift, BufferedInput) +{ + ClockDrift c(48000, 48000, 5 * 480); + EXPECT_EQ(c.GetCorrection(), 1.0); + + EXPECT_FLOAT_EQ(RunUntilCorrectionUpdate(c, 480, 480, 5 * 480, 8 * 480), 1.0); + + c.UpdateClock(480, 480, 0, 10 * 480); // 0 buffered when updating correction + EXPECT_FLOAT_EQ(c.GetCorrection(), 1.0473685); + EXPECT_FLOAT_EQ( + RunUntilCorrectionUpdate(c, 480, 480, 3 * 480, 7 * 480, 480, 480), + 1.0473685); + + c.UpdateClock(480, 480, 3 * 480, 7 * 480); + EXPECT_FLOAT_EQ(c.GetCorrection(), 1.0311923); + EXPECT_FLOAT_EQ( + RunUntilCorrectionUpdate(c, 480, 480, 5 * 480, 5 * 480, 480, 480), + 1.0311923); + + c.UpdateClock(480, 480, 5 * 480, 5 * 480); + EXPECT_FLOAT_EQ(c.GetCorrection(), 1.0124769); + EXPECT_FLOAT_EQ( + RunUntilCorrectionUpdate(c, 480, 480, 7 * 480, 3 * 480, 480, 480), + 1.0124769); + + c.UpdateClock(480, 480, 7 * 480, 3 * 480); + EXPECT_FLOAT_EQ(c.GetCorrection(), 0.99322605); +} + +TEST(TestClockDrift, BufferedInputWithResampling) +{ + ClockDrift c(24000, 48000, 5 * 240); + EXPECT_EQ(c.GetCorrection(), 1.0); + + EXPECT_FLOAT_EQ(RunUntilCorrectionUpdate(c, 240, 480, 5 * 240, 5 * 240), 1.0); + + c.UpdateClock(240, 480, 0, 10 * 240); // 0 buffered when updating correction + EXPECT_FLOAT_EQ(c.GetCorrection(), 1.0473685); + EXPECT_FLOAT_EQ( + RunUntilCorrectionUpdate(c, 240, 480, 3 * 240, 7 * 240, 240, 480), + 1.0473685); + + c.UpdateClock(240, 480, 3 * 240, 7 * 240); + EXPECT_FLOAT_EQ(c.GetCorrection(), 1.0311923); + EXPECT_FLOAT_EQ( + RunUntilCorrectionUpdate(c, 240, 480, 5 * 240, 5 * 240, 240, 480), + 1.0311923); + + c.UpdateClock(240, 480, 5 * 240, 5 * 240); + EXPECT_FLOAT_EQ(c.GetCorrection(), 1.0124769); + EXPECT_FLOAT_EQ( + RunUntilCorrectionUpdate(c, 240, 480, 7 * 240, 3 * 240, 240, 480), + 1.0124769); + + c.UpdateClock(240, 480, 7 * 240, 3 * 240); + EXPECT_FLOAT_EQ(c.GetCorrection(), 0.99322605); +} + +TEST(TestClockDrift, Clamp) +{ + // Keep buffered frames to the wanted level in order to not affect that test. + const uint32_t buffered = 5 * 480; + + ClockDrift c(48000, 48000, buffered); + + // +30% + EXPECT_FLOAT_EQ( + RunUntilCorrectionUpdate(c, 480, 480 + 3 * 48, buffered, buffered), 1.0); + + // -30% + EXPECT_FLOAT_EQ( + RunUntilCorrectionUpdate(c, 480, 480 - 3 * 48, buffered, buffered), 1.1); + + c.UpdateClock(0, 0, buffered, buffered); + EXPECT_FLOAT_EQ(c.GetCorrection(), 0.9); +} + +TEST(TestClockDrift, SmallDiff) +{ + // Keep buffered frames to the wanted level in order to not affect that test. + const uint32_t buffered = 5 * 480; + + ClockDrift c(48000, 48000, buffered); + + EXPECT_FLOAT_EQ(RunUntilCorrectionUpdate(c, 480 + 4, 480, buffered, buffered), + 1.0); + EXPECT_FLOAT_EQ(RunUntilCorrectionUpdate(c, 480 + 5, 480, buffered, buffered), + 0.99504131); + EXPECT_FLOAT_EQ(RunUntilCorrectionUpdate(c, 480, 480, buffered, buffered), + 0.991831); + EXPECT_FLOAT_EQ(RunUntilCorrectionUpdate(c, 480, 480 + 4, buffered, buffered), + 0.99673241); + c.UpdateClock(0, 0, buffered, buffered); + EXPECT_FLOAT_EQ(c.GetCorrection(), 1.003693); +} + +TEST(TestClockDrift, SmallBufferedFrames) +{ + ClockDrift c(48000, 48000, 5 * 480); + + EXPECT_FLOAT_EQ(c.GetCorrection(), 1.0); + for (uint32_t i = 0; i < 10; ++i) { + c.UpdateClock(480, 480, 5 * 480, 5 * 480); + } + EXPECT_FLOAT_EQ(c.GetCorrection(), 1.0); + c.UpdateClock(480, 480, 0, 10 * 480); + EXPECT_FLOAT_EQ(c.GetCorrection(), 1.1); + + EXPECT_FLOAT_EQ( + RunUntilCorrectionUpdate(c, 480, 480, 5 * 480, 5 * 480, 24000, 24000), + 1.1); + c.UpdateClock(480, 480, 0, 10 * 480); + EXPECT_FLOAT_EQ(c.GetCorrection(), 1.1); +} + +// Print the mono channel of a segment. +void printAudioSegment(const AudioSegment& segment) { + for (AudioSegment::ConstChunkIterator iter(segment); !iter.IsEnded(); + iter.Next()) { + const AudioChunk& c = *iter; + for (uint32_t i = 0; i < c.GetDuration(); ++i) { + if (c.mBufferFormat == AUDIO_FORMAT_FLOAT32) { + printf("%f\n", c.ChannelData<float>()[0][i]); + } else { + printf("%d\n", c.ChannelData<int16_t>()[0][i]); + } + } + } +} + +template <class T> +AudioChunk CreateAudioChunk(uint32_t aFrames, uint32_t aChannels, + AudioSampleFormat aSampleFormat); + +void testAudioCorrection(int32_t aSourceRate, int32_t aTargetRate) { + const uint32_t channels = 1; + const uint32_t sampleRateTransmitter = aSourceRate; + const uint32_t sampleRateReceiver = aTargetRate; + const uint32_t frequency = 100; + AudioDriftCorrection ad(sampleRateTransmitter, sampleRateReceiver); + + AudioGenerator<AudioDataValue> tone(channels, sampleRateTransmitter, + frequency); + AudioVerifier<AudioDataValue> inToneVerifier(sampleRateTransmitter, + frequency); + AudioVerifier<AudioDataValue> outToneVerifier(sampleRateReceiver, frequency); + + uint32_t sourceFrames; + const uint32_t targetFrames = sampleRateReceiver / 100; + + // Run for some time: 3 * 1050 = 3150 iterations + for (uint32_t j = 0; j < 3; ++j) { + // apply some drift + if (j % 2 == 0) { + sourceFrames = + sampleRateTransmitter * /*1.02*/ 102 / 100 / /*1s->10ms*/ 100; + } else { + sourceFrames = + sampleRateTransmitter * /*0.98*/ 98 / 100 / /*1s->10ms*/ 100; + } + + // 10.5 seconds, allows for at least 10 correction changes, to stabilize + // around the desired buffer. + for (uint32_t n = 0; n < 1050; ++n) { + // Create the input (sine tone) + AudioSegment inSegment; + tone.Generate(inSegment, sourceFrames); + inToneVerifier.AppendData(inSegment); + // Print the input for debugging + // printAudioSegment(inSegment); + + // Get the output of the correction + AudioSegment outSegment = ad.RequestFrames(inSegment, targetFrames); + EXPECT_EQ(outSegment.GetDuration(), targetFrames); + // Print the output for debugging + // printAudioSegment(outSegment); + outToneVerifier.AppendData(outSegment); + } + } + + const int32_t expectedBuffering = + ad.mDesiredBuffering - sampleRateTransmitter / 100 /* 10ms */; + EXPECT_NEAR(ad.CurrentBuffering(), expectedBuffering, 512); + + EXPECT_NEAR(inToneVerifier.EstimatedFreq(), tone.mFrequency, 1.0f); + EXPECT_EQ(inToneVerifier.PreSilenceSamples(), 0U); + EXPECT_EQ(inToneVerifier.CountDiscontinuities(), 0U); + + EXPECT_NEAR(outToneVerifier.EstimatedFreq(), tone.mFrequency, 1.0f); + // The expected pre-silence is 50ms plus the resampling. + EXPECT_GE(outToneVerifier.PreSilenceSamples(), aTargetRate * 50 / 1000U); + EXPECT_EQ(outToneVerifier.CountDiscontinuities(), 0U); +} + +TEST(TestAudioDriftCorrection, Basic) +{ + printf("Testing AudioCorrection 48 -> 48\n"); + testAudioCorrection(48000, 48000); + printf("Testing AudioCorrection 48 -> 44.1\n"); + testAudioCorrection(48000, 44100); + printf("Testing AudioCorrection 44.1 -> 48\n"); + testAudioCorrection(44100, 48000); + printf("Testing AudioCorrection 23458 -> 25113\n"); + testAudioCorrection(23458, 25113); +} + +void testMonoToStereoInput(uint32_t aSourceRate, uint32_t aTargetRate) { + const uint32_t frequency = 100; + const uint32_t sampleRateTransmitter = aSourceRate; + const uint32_t sampleRateReceiver = aTargetRate; + AudioDriftCorrection ad(sampleRateTransmitter, sampleRateReceiver); + + AudioGenerator<AudioDataValue> monoTone(1, sampleRateTransmitter, frequency); + AudioGenerator<AudioDataValue> stereoTone(2, sampleRateTransmitter, + frequency); + AudioVerifier<AudioDataValue> inToneVerify(sampleRateTransmitter, frequency); + AudioVerifier<AudioDataValue> outToneVerify(sampleRateReceiver, frequency); + + uint32_t sourceFrames; + const uint32_t targetFrames = sampleRateReceiver / 100; + + // Run for some time: 6 * 250 = 1500 iterations + for (uint32_t j = 0; j < 6; ++j) { + // apply some drift + if (j % 2 == 0) { + sourceFrames = sampleRateTransmitter / 100 + 10; + } else { + sourceFrames = sampleRateTransmitter / 100 - 10; + } + + for (uint32_t n = 0; n < 250; ++n) { + // Create the input (sine tone) of two chunks. + AudioSegment inSegment; + monoTone.Generate(inSegment, sourceFrames / 2); + stereoTone.SetOffset(monoTone.Offset()); + stereoTone.Generate(inSegment, sourceFrames / 2); + monoTone.SetOffset(stereoTone.Offset()); + inToneVerify.AppendData(inSegment); + // Print the input for debugging + // printAudioSegment(inSegment); + + // Get the output of the correction + AudioSegment outSegment = ad.RequestFrames(inSegment, targetFrames); + EXPECT_EQ(outSegment.GetDuration(), targetFrames); + // Print the output for debugging + // printAudioSegment(outSegment); + outToneVerify.AppendData(outSegment); + } + } + EXPECT_EQ(inToneVerify.EstimatedFreq(), frequency); + EXPECT_EQ(inToneVerify.PreSilenceSamples(), 0U); + EXPECT_EQ(inToneVerify.CountDiscontinuities(), 0U); + + EXPECT_GT(outToneVerify.CountDiscontinuities(), 0U) + << "Expect discontinuities"; + EXPECT_NE(outToneVerify.EstimatedFreq(), frequency) + << "Estimation is not accurate due to discontinuities"; + // The expected pre-silence is 50ms plus the resampling. However, due to + // discontinuities pre-silence is expected only in the first iteration which + // is routhly a little more than 400 frames for the chosen sample rates. + EXPECT_GT(outToneVerify.PreSilenceSamples(), 400U); +} + +TEST(TestAudioDriftCorrection, MonoToStereoInput) +{ + testMonoToStereoInput(48000, 48000); + testMonoToStereoInput(48000, 44100); + testMonoToStereoInput(44100, 48000); +} + +TEST(TestAudioDriftCorrection, NotEnoughFrames) +{ + const uint32_t sampleRateTransmitter = 48000; + const uint32_t sampleRateReceiver = 48000; + AudioDriftCorrection ad(sampleRateTransmitter, sampleRateReceiver); + const uint32_t targetFrames = sampleRateReceiver / 100; + + for (uint32_t i = 0; i < 7; ++i) { + // Input is something small, 10 frames here, in order to dry out fast, + // after 4 iterations + AudioChunk chunk = CreateAudioChunk<float>(10, 1, AUDIO_FORMAT_FLOAT32); + AudioSegment inSegment; + inSegment.AppendAndConsumeChunk(&chunk); + + AudioSegment outSegment = ad.RequestFrames(inSegment, targetFrames); + EXPECT_EQ(outSegment.GetDuration(), targetFrames); + if (i < 5) { + EXPECT_FALSE(outSegment.IsNull()); + } else { + // Last 2 iterations, the 5th and 6th, will be null. It has used all + // buffered data so the output is silence. + EXPECT_TRUE(outSegment.IsNull()); + } + } +} + +TEST(TestAudioDriftCorrection, CrashInAudioResampler) +{ + const uint32_t sampleRateTransmitter = 48000; + const uint32_t sampleRateReceiver = 48000; + AudioDriftCorrection ad(sampleRateTransmitter, sampleRateReceiver); + const uint32_t targetFrames = sampleRateReceiver / 100; + + for (uint32_t i = 0; i < 100; ++i) { + AudioChunk chunk = CreateAudioChunk<float>(sampleRateTransmitter / 1000, 1, + AUDIO_FORMAT_FLOAT32); + AudioSegment inSegment; + inSegment.AppendAndConsumeChunk(&chunk); + + AudioSegment outSegment = ad.RequestFrames(inSegment, targetFrames); + EXPECT_EQ(outSegment.GetDuration(), targetFrames); + } +} diff --git a/dom/media/gtest/TestAudioMixer.cpp b/dom/media/gtest/TestAudioMixer.cpp new file mode 100644 index 0000000000..017ac960eb --- /dev/null +++ b/dom/media/gtest/TestAudioMixer.cpp @@ -0,0 +1,174 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AudioMixer.h" +#include "gtest/gtest.h" + +using mozilla::AudioDataValue; +using mozilla::AudioSampleFormat; + +namespace audio_mixer { + +struct MixerConsumer : public mozilla::MixerCallbackReceiver { + /* In this test, the different audio stream and channels are always created to + * cancel each other. */ + void MixerCallback(AudioDataValue* aData, AudioSampleFormat aFormat, + uint32_t aChannels, uint32_t aFrames, + uint32_t aSampleRate) { + bool silent = true; + for (uint32_t i = 0; i < aChannels * aFrames; i++) { + if (aData[i] != 0.0) { + if (aFormat == mozilla::AUDIO_FORMAT_S16) { + fprintf(stderr, "Sample at %d is not silent: %d\n", i, + (short)aData[i]); + } else { + fprintf(stderr, "Sample at %d is not silent: %f\n", i, + (float)aData[i]); + } + silent = false; + } + } + ASSERT_TRUE(silent); + } +}; + +/* Helper function to give us the maximum and minimum value that don't clip, + * for a given sample format (integer or floating-point). */ +template <typename T> +T GetLowValue(); + +template <typename T> +T GetHighValue(); + +template <> +float GetLowValue<float>() { + return -1.0; +} + +template <> +short GetLowValue<short>() { + return -INT16_MAX; +} + +template <> +float GetHighValue<float>() { + return 1.0; +} + +template <> +short GetHighValue<short>() { + return INT16_MAX; +} + +void FillBuffer(AudioDataValue* aBuffer, uint32_t aLength, + AudioDataValue aValue) { + AudioDataValue* end = aBuffer + aLength; + while (aBuffer != end) { + *aBuffer++ = aValue; + } +} + +TEST(AudioMixer, Test) +{ + const uint32_t CHANNEL_LENGTH = 256; + const uint32_t AUDIO_RATE = 44100; + MixerConsumer consumer; + AudioDataValue a[CHANNEL_LENGTH * 2]; + AudioDataValue b[CHANNEL_LENGTH * 2]; + FillBuffer(a, CHANNEL_LENGTH, GetLowValue<AudioDataValue>()); + FillBuffer(a + CHANNEL_LENGTH, CHANNEL_LENGTH, + GetHighValue<AudioDataValue>()); + FillBuffer(b, CHANNEL_LENGTH, GetHighValue<AudioDataValue>()); + FillBuffer(b + CHANNEL_LENGTH, CHANNEL_LENGTH, GetLowValue<AudioDataValue>()); + + { + int iterations = 2; + mozilla::AudioMixer mixer; + mixer.AddCallback(WrapNotNull(&consumer)); + + fprintf(stderr, "Test AudioMixer constant buffer length.\n"); + + while (iterations--) { + mixer.Mix(a, 2, CHANNEL_LENGTH, AUDIO_RATE); + mixer.Mix(b, 2, CHANNEL_LENGTH, AUDIO_RATE); + mixer.FinishMixing(); + } + } + + { + mozilla::AudioMixer mixer; + mixer.AddCallback(WrapNotNull(&consumer)); + + fprintf(stderr, "Test AudioMixer variable buffer length.\n"); + + FillBuffer(a, CHANNEL_LENGTH / 2, GetLowValue<AudioDataValue>()); + FillBuffer(a + CHANNEL_LENGTH / 2, CHANNEL_LENGTH / 2, + GetLowValue<AudioDataValue>()); + FillBuffer(b, CHANNEL_LENGTH / 2, GetHighValue<AudioDataValue>()); + FillBuffer(b + CHANNEL_LENGTH / 2, CHANNEL_LENGTH / 2, + GetHighValue<AudioDataValue>()); + mixer.Mix(a, 2, CHANNEL_LENGTH / 2, AUDIO_RATE); + mixer.Mix(b, 2, CHANNEL_LENGTH / 2, AUDIO_RATE); + mixer.FinishMixing(); + FillBuffer(a, CHANNEL_LENGTH, GetLowValue<AudioDataValue>()); + FillBuffer(a + CHANNEL_LENGTH, CHANNEL_LENGTH, + GetHighValue<AudioDataValue>()); + FillBuffer(b, CHANNEL_LENGTH, GetHighValue<AudioDataValue>()); + FillBuffer(b + CHANNEL_LENGTH, CHANNEL_LENGTH, + GetLowValue<AudioDataValue>()); + mixer.Mix(a, 2, CHANNEL_LENGTH, AUDIO_RATE); + mixer.Mix(b, 2, CHANNEL_LENGTH, AUDIO_RATE); + mixer.FinishMixing(); + FillBuffer(a, CHANNEL_LENGTH / 2, GetLowValue<AudioDataValue>()); + FillBuffer(a + CHANNEL_LENGTH / 2, CHANNEL_LENGTH / 2, + GetLowValue<AudioDataValue>()); + FillBuffer(b, CHANNEL_LENGTH / 2, GetHighValue<AudioDataValue>()); + FillBuffer(b + CHANNEL_LENGTH / 2, CHANNEL_LENGTH / 2, + GetHighValue<AudioDataValue>()); + mixer.Mix(a, 2, CHANNEL_LENGTH / 2, AUDIO_RATE); + mixer.Mix(b, 2, CHANNEL_LENGTH / 2, AUDIO_RATE); + mixer.FinishMixing(); + } + + FillBuffer(a, CHANNEL_LENGTH, GetLowValue<AudioDataValue>()); + FillBuffer(b, CHANNEL_LENGTH, GetHighValue<AudioDataValue>()); + + { + mozilla::AudioMixer mixer; + mixer.AddCallback(WrapNotNull(&consumer)); + + fprintf(stderr, "Test AudioMixer variable channel count.\n"); + + mixer.Mix(a, 1, CHANNEL_LENGTH, AUDIO_RATE); + mixer.Mix(b, 1, CHANNEL_LENGTH, AUDIO_RATE); + mixer.FinishMixing(); + mixer.Mix(a, 1, CHANNEL_LENGTH, AUDIO_RATE); + mixer.Mix(b, 1, CHANNEL_LENGTH, AUDIO_RATE); + mixer.FinishMixing(); + mixer.Mix(a, 1, CHANNEL_LENGTH, AUDIO_RATE); + mixer.Mix(b, 1, CHANNEL_LENGTH, AUDIO_RATE); + mixer.FinishMixing(); + } + + { + mozilla::AudioMixer mixer; + mixer.AddCallback(WrapNotNull(&consumer)); + fprintf(stderr, "Test AudioMixer variable stream count.\n"); + + mixer.Mix(a, 2, CHANNEL_LENGTH, AUDIO_RATE); + mixer.Mix(b, 2, CHANNEL_LENGTH, AUDIO_RATE); + mixer.FinishMixing(); + mixer.Mix(a, 2, CHANNEL_LENGTH, AUDIO_RATE); + mixer.Mix(b, 2, CHANNEL_LENGTH, AUDIO_RATE); + mixer.Mix(a, 2, CHANNEL_LENGTH, AUDIO_RATE); + mixer.Mix(b, 2, CHANNEL_LENGTH, AUDIO_RATE); + mixer.FinishMixing(); + mixer.Mix(a, 2, CHANNEL_LENGTH, AUDIO_RATE); + mixer.Mix(b, 2, CHANNEL_LENGTH, AUDIO_RATE); + mixer.FinishMixing(); + } +} + +} // namespace audio_mixer diff --git a/dom/media/gtest/TestAudioPacketizer.cpp b/dom/media/gtest/TestAudioPacketizer.cpp new file mode 100644 index 0000000000..6c3275d82a --- /dev/null +++ b/dom/media/gtest/TestAudioPacketizer.cpp @@ -0,0 +1,163 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <stdint.h> +#include <math.h> +#include <memory> +#include "../AudioPacketizer.h" +#include "gtest/gtest.h" + +using namespace mozilla; + +template <typename T> +class AutoBuffer { + public: + explicit AutoBuffer(size_t aLength) { mStorage = new T[aLength]; } + ~AutoBuffer() { delete[] mStorage; } + T* Get() { return mStorage; } + + private: + T* mStorage; +}; + +int16_t Sequence(int16_t* aBuffer, uint32_t aSize, uint32_t aStart = 0) { + uint32_t i; + for (i = 0; i < aSize; i++) { + aBuffer[i] = aStart + i; + } + return aStart + i; +} + +void IsSequence(std::unique_ptr<int16_t[]> aBuffer, uint32_t aSize, + uint32_t aStart = 0) { + for (uint32_t i = 0; i < aSize; i++) { + ASSERT_TRUE(aBuffer[i] == static_cast<int64_t>(aStart + i)) + << "Buffer is not a sequence at offset " << i << std::endl; + } + // Buffer is a sequence. +} + +void Zero(std::unique_ptr<int16_t[]> aBuffer, uint32_t aSize) { + for (uint32_t i = 0; i < aSize; i++) { + ASSERT_TRUE(aBuffer[i] == 0) + << "Buffer is not null at offset " << i << std::endl; + } +} + +double sine(uint32_t aPhase) { return sin(aPhase * 2 * M_PI * 440 / 44100); } + +TEST(AudioPacketizer, Test) +{ + for (int16_t channels = 1; channels < 2; channels++) { + // Test that the packetizer returns zero on underrun + { + AudioPacketizer<int16_t, int16_t> ap(441, channels); + for (int16_t i = 0; i < 10; i++) { + std::unique_ptr<int16_t[]> out(ap.Output()); + Zero(std::move(out), 441); + } + } + // Simple test, with input/output buffer size aligned on the packet size, + // alternating Input and Output calls. + { + AudioPacketizer<int16_t, int16_t> ap(441, channels); + int16_t seqEnd = 0; + for (int16_t i = 0; i < 10; i++) { + AutoBuffer<int16_t> b(441 * channels); + int16_t prevEnd = seqEnd; + seqEnd = Sequence(b.Get(), channels * 441, prevEnd); + ap.Input(b.Get(), 441); + std::unique_ptr<int16_t[]> out(ap.Output()); + IsSequence(std::move(out), 441 * channels, prevEnd); + } + } + // Simple test, with input/output buffer size aligned on the packet size, + // alternating two Input and Output calls. + { + AudioPacketizer<int16_t, int16_t> ap(441, channels); + int16_t seqEnd = 0; + for (int16_t i = 0; i < 10; i++) { + AutoBuffer<int16_t> b(441 * channels); + AutoBuffer<int16_t> b1(441 * channels); + int16_t prevEnd0 = seqEnd; + seqEnd = Sequence(b.Get(), 441 * channels, prevEnd0); + int16_t prevEnd1 = seqEnd; + seqEnd = Sequence(b1.Get(), 441 * channels, seqEnd); + ap.Input(b.Get(), 441); + ap.Input(b1.Get(), 441); + std::unique_ptr<int16_t[]> out(ap.Output()); + std::unique_ptr<int16_t[]> out2(ap.Output()); + IsSequence(std::move(out), 441 * channels, prevEnd0); + IsSequence(std::move(out2), 441 * channels, prevEnd1); + } + } + // Input/output buffer size not aligned on the packet size, + // alternating two Input and Output calls. + { + AudioPacketizer<int16_t, int16_t> ap(441, channels); + int16_t prevEnd = 0; + int16_t prevSeq = 0; + for (int16_t i = 0; i < 10; i++) { + AutoBuffer<int16_t> b(480 * channels); + AutoBuffer<int16_t> b1(480 * channels); + prevSeq = Sequence(b.Get(), 480 * channels, prevSeq); + prevSeq = Sequence(b1.Get(), 480 * channels, prevSeq); + ap.Input(b.Get(), 480); + ap.Input(b1.Get(), 480); + std::unique_ptr<int16_t[]> out(ap.Output()); + std::unique_ptr<int16_t[]> out2(ap.Output()); + IsSequence(std::move(out), 441 * channels, prevEnd); + prevEnd += 441 * channels; + IsSequence(std::move(out2), 441 * channels, prevEnd); + prevEnd += 441 * channels; + } + printf("Available: %d\n", ap.PacketsAvailable()); + } + + // "Real-life" test case: streaming a sine wave through a packetizer, and + // checking that we have the right output. + // 128 is, for example, the size of a Web Audio API block, and 441 is the + // size of a webrtc.org packet when the sample rate is 44100 (10ms) + { + AudioPacketizer<int16_t, int16_t> ap(441, channels); + AutoBuffer<int16_t> b(128 * channels); + uint32_t phase = 0; + uint32_t outPhase = 0; + for (int16_t i = 0; i < 1000; i++) { + for (int32_t j = 0; j < 128; j++) { + for (int32_t c = 0; c < channels; c++) { + // int16_t sinewave at 440Hz/44100Hz sample rate + b.Get()[j * channels + c] = (2 << 14) * sine(phase); + } + phase++; + } + ap.Input(b.Get(), 128); + while (ap.PacketsAvailable()) { + std::unique_ptr<int16_t[]> packet(ap.Output()); + for (uint32_t k = 0; k < ap.mPacketSize; k++) { + for (int32_t c = 0; c < channels; c++) { + ASSERT_TRUE(packet[k * channels + c] == + static_cast<int16_t>(((2 << 14) * sine(outPhase)))); + } + outPhase++; + } + } + } + } + // Test that clearing the packetizer empties it and starts returning zeros. + { + AudioPacketizer<int16_t, int16_t> ap(441, channels); + AutoBuffer<int16_t> b(440 * channels); + Sequence(b.Get(), 440 * channels); + ap.Input(b.Get(), 440); + EXPECT_EQ(ap.FramesAvailable(), 440U); + ap.Clear(); + EXPECT_EQ(ap.FramesAvailable(), 0U); + EXPECT_TRUE(ap.Empty()); + std::unique_ptr<int16_t[]> out(ap.Output()); + Zero(std::move(out), 441); + } + } +} diff --git a/dom/media/gtest/TestAudioRingBuffer.cpp b/dom/media/gtest/TestAudioRingBuffer.cpp new file mode 100644 index 0000000000..1eb33df384 --- /dev/null +++ b/dom/media/gtest/TestAudioRingBuffer.cpp @@ -0,0 +1,993 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AudioRingBuffer.h" + +#include "gtest/gtest.h" +#include "mozilla/PodOperations.h" + +using namespace mozilla; + +TEST(TestAudioRingBuffer, BasicFloat) +{ + AudioRingBuffer ringBuffer(11 * sizeof(float)); + ringBuffer.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + + uint32_t rv = ringBuffer.WriteSilence(4); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 6u); + EXPECT_EQ(ringBuffer.AvailableRead(), 4u); + + float in[4] = {.1, .2, .3, .4}; + rv = ringBuffer.Write(Span(in, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 2u); + EXPECT_EQ(ringBuffer.AvailableRead(), 8u); + + rv = ringBuffer.WriteSilence(4); + EXPECT_EQ(rv, 2u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 0u); + EXPECT_EQ(ringBuffer.AvailableRead(), 10u); + + rv = ringBuffer.Write(Span(in, 4)); + EXPECT_EQ(rv, 0u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 0u); + EXPECT_EQ(ringBuffer.AvailableRead(), 10u); + + float out[4] = {}; + rv = ringBuffer.Read(Span(out, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 4u); + EXPECT_EQ(ringBuffer.AvailableRead(), 6u); + for (float f : out) { + EXPECT_FLOAT_EQ(f, 0.0); + } + + rv = ringBuffer.Read(Span(out, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 8u); + EXPECT_EQ(ringBuffer.AvailableRead(), 2u); + for (uint32_t i = 0; i < 4; ++i) { + EXPECT_FLOAT_EQ(in[i], out[i]); + } + + rv = ringBuffer.Read(Span(out, 4)); + EXPECT_EQ(rv, 2u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + for (uint32_t i = 0; i < 2; ++i) { + EXPECT_FLOAT_EQ(out[i], 0.0); + } + + rv = ringBuffer.Clear(); + EXPECT_EQ(rv, 0u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); +} + +TEST(TestAudioRingBuffer, BasicShort) +{ + AudioRingBuffer ringBuffer(11 * sizeof(short)); + ringBuffer.SetSampleFormat(AUDIO_FORMAT_S16); + + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + + uint32_t rv = ringBuffer.WriteSilence(4); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 6u); + EXPECT_EQ(ringBuffer.AvailableRead(), 4u); + + short in[4] = {1, 2, 3, 4}; + rv = ringBuffer.Write(Span(in, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 2u); + EXPECT_EQ(ringBuffer.AvailableRead(), 8u); + + rv = ringBuffer.WriteSilence(4); + EXPECT_EQ(rv, 2u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 0u); + EXPECT_EQ(ringBuffer.AvailableRead(), 10u); + + rv = ringBuffer.Write(Span(in, 4)); + EXPECT_EQ(rv, 0u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 0u); + EXPECT_EQ(ringBuffer.AvailableRead(), 10u); + + short out[4] = {}; + rv = ringBuffer.Read(Span(out, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 4u); + EXPECT_EQ(ringBuffer.AvailableRead(), 6u); + for (float f : out) { + EXPECT_EQ(f, 0); + } + + rv = ringBuffer.Read(Span(out, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 8u); + EXPECT_EQ(ringBuffer.AvailableRead(), 2u); + for (uint32_t i = 0; i < 4; ++i) { + EXPECT_EQ(in[i], out[i]); + } + + rv = ringBuffer.Read(Span(out, 4)); + EXPECT_EQ(rv, 2u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + for (uint32_t i = 0; i < 2; ++i) { + EXPECT_EQ(out[i], 0); + } + + rv = ringBuffer.Clear(); + EXPECT_EQ(rv, 0u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); +} + +TEST(TestAudioRingBuffer, BasicFloat2) +{ + AudioRingBuffer ringBuffer(11 * sizeof(float)); + ringBuffer.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + + float in[4] = {.1, .2, .3, .4}; + uint32_t rv = ringBuffer.Write(Span(in, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 6u); + EXPECT_EQ(ringBuffer.AvailableRead(), 4u); + + rv = ringBuffer.Write(Span(in, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 2u); + EXPECT_EQ(ringBuffer.AvailableRead(), 8u); + + float out[4] = {}; + rv = ringBuffer.Read(Span(out, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 6u); + EXPECT_EQ(ringBuffer.AvailableRead(), 4u); + for (uint32_t i = 0; i < 4; ++i) { + EXPECT_FLOAT_EQ(in[i], out[i]); + } + + // WriteIndex = 12 + rv = ringBuffer.Write(Span(in, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 2u); + EXPECT_EQ(ringBuffer.AvailableRead(), 8u); + + rv = ringBuffer.Read(Span(out, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 6u); + EXPECT_EQ(ringBuffer.AvailableRead(), 4u); + for (uint32_t i = 0; i < 4; ++i) { + EXPECT_FLOAT_EQ(in[i], out[i]); + } + + rv = ringBuffer.Read(Span(out, 8)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + for (uint32_t i = 0; i < 4; ++i) { + EXPECT_FLOAT_EQ(in[i], out[i]); + } + + rv = ringBuffer.Read(Span(out, 8)); + EXPECT_EQ(rv, 0u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + for (uint32_t i = 0; i < 4; ++i) { + EXPECT_FLOAT_EQ(in[i], out[i]); + } + + // WriteIndex = 16 + rv = ringBuffer.Write(Span(in, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 6u); + EXPECT_EQ(ringBuffer.AvailableRead(), 4u); + + rv = ringBuffer.Write(Span(in, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 2u); + EXPECT_EQ(ringBuffer.AvailableRead(), 8u); + + rv = ringBuffer.Write(Span(in, 4)); + EXPECT_EQ(rv, 2u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 0u); + EXPECT_EQ(ringBuffer.AvailableRead(), 10u); + + rv = ringBuffer.Write(Span(in, 4)); + EXPECT_EQ(rv, 0u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 0u); + EXPECT_EQ(ringBuffer.AvailableRead(), 10u); +} + +TEST(TestAudioRingBuffer, BasicShort2) +{ + AudioRingBuffer ringBuffer(11 * sizeof(int16_t)); + ringBuffer.SetSampleFormat(AUDIO_FORMAT_S16); + + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + + int16_t in[4] = {1, 2, 3, 4}; + uint32_t rv = ringBuffer.Write(Span(in, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 6u); + EXPECT_EQ(ringBuffer.AvailableRead(), 4u); + + rv = ringBuffer.Write(Span(in, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 2u); + EXPECT_EQ(ringBuffer.AvailableRead(), 8u); + + int16_t out[4] = {}; + rv = ringBuffer.Read(Span(out, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 6u); + EXPECT_EQ(ringBuffer.AvailableRead(), 4u); + for (uint32_t i = 0; i < 4; ++i) { + EXPECT_EQ(in[i], out[i]); + } + + // WriteIndex = 12 + rv = ringBuffer.Write(Span(in, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 2u); + EXPECT_EQ(ringBuffer.AvailableRead(), 8u); + + rv = ringBuffer.Read(Span(out, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 6u); + EXPECT_EQ(ringBuffer.AvailableRead(), 4u); + for (uint32_t i = 0; i < 4; ++i) { + EXPECT_EQ(in[i], out[i]); + } + + rv = ringBuffer.Read(Span(out, 8)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + for (uint32_t i = 0; i < 4; ++i) { + EXPECT_EQ(in[i], out[i]); + } + + rv = ringBuffer.Read(Span(out, 8)); + EXPECT_EQ(rv, 0u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + for (uint32_t i = 0; i < 4; ++i) { + EXPECT_EQ(in[i], out[i]); + } + + // WriteIndex = 16 + rv = ringBuffer.Write(Span(in, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 6u); + EXPECT_EQ(ringBuffer.AvailableRead(), 4u); + + rv = ringBuffer.Write(Span(in, 4)); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 2u); + EXPECT_EQ(ringBuffer.AvailableRead(), 8u); + + rv = ringBuffer.Write(Span(in, 4)); + EXPECT_EQ(rv, 2u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 0u); + EXPECT_EQ(ringBuffer.AvailableRead(), 10u); + + rv = ringBuffer.Write(Span(in, 4)); + EXPECT_EQ(rv, 0u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 0u); + EXPECT_EQ(ringBuffer.AvailableRead(), 10u); +} + +TEST(TestAudioRingBuffer, NoCopyFloat) +{ + AudioRingBuffer ringBuffer(11 * sizeof(float)); + ringBuffer.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + + float in[8] = {.0, .1, .2, .3, .4, .5, .6, .7}; + ringBuffer.Write(Span(in, 6)); + // v ReadIndex + // [x0: .0, x1: .1, x2: .2, x3: .3, x4: .4, + // x5: .5, x6: .0, x7: .0, x8: .0, x9: .0, x10: .0] + + float out[10] = {}; + float* out_ptr = out; + + uint32_t rv = + ringBuffer.ReadNoCopy([&out_ptr](const Span<const float> aInBuffer) { + PodMove(out_ptr, aInBuffer.data(), aInBuffer.Length()); + out_ptr += aInBuffer.Length(); + return aInBuffer.Length(); + }); + EXPECT_EQ(rv, 6u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_FLOAT_EQ(out[i], in[i]); + } + + ringBuffer.Write(Span(in, 8)); + // Now the buffer contains: + // [x0: .5, x1: .6, x2: .2, x3: .3, x4: .4, + // x5: .5, x6: .0, x7: .1, x8: .2, x9: .3, x10: .4 + // ^ ReadIndex + out_ptr = out; // reset the pointer before lambdas reuse + rv = ringBuffer.ReadNoCopy([&out_ptr](const Span<const float> aInBuffer) { + PodMove(out_ptr, aInBuffer.data(), aInBuffer.Length()); + out_ptr += aInBuffer.Length(); + return aInBuffer.Length(); + }); + EXPECT_EQ(rv, 8u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_FLOAT_EQ(out[i], in[i]); + } +} + +TEST(TestAudioRingBuffer, NoCopyShort) +{ + AudioRingBuffer ringBuffer(11 * sizeof(short)); + ringBuffer.SetSampleFormat(AUDIO_FORMAT_S16); + + short in[8] = {0, 1, 2, 3, 4, 5, 6, 7}; + ringBuffer.Write(Span(in, 6)); + // v ReadIndex + // [x0: 0, x1: 1, x2: 2, x3: 3, x4: 4, + // x5: 5, x6: 0, x7: 0, x8: 0, x9: 0, x10: 0] + + short out[10] = {}; + short* out_ptr = out; + + uint32_t rv = + ringBuffer.ReadNoCopy([&out_ptr](const Span<const short> aInBuffer) { + PodMove(out_ptr, aInBuffer.data(), aInBuffer.Length()); + out_ptr += aInBuffer.Length(); + return aInBuffer.Length(); + }); + EXPECT_EQ(rv, 6u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_EQ(out[i], in[i]); + } + + ringBuffer.Write(Span(in, 8)); + // Now the buffer contains: + // [x0: 5, x1: 6, x2: 2, x3: 3, x4: 4, + // x5: 5, x6: 0, x7: 1, x8: 2, x9: 3, x10: 4 + // ^ ReadIndex + out_ptr = out; // reset the pointer before lambdas reuse + rv = ringBuffer.ReadNoCopy([&out_ptr](const Span<const short> aInBuffer) { + PodMove(out_ptr, aInBuffer.data(), aInBuffer.Length()); + out_ptr += aInBuffer.Length(); + return aInBuffer.Length(); + }); + EXPECT_EQ(rv, 8u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_EQ(out[i], in[i]); + } +} + +TEST(TestAudioRingBuffer, NoCopyFloat2) +{ + AudioRingBuffer ringBuffer(11 * sizeof(float)); + ringBuffer.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + + float in[8] = {.0, .1, .2, .3, .4, .5, .6, .7}; + ringBuffer.Write(Span(in, 6)); + // v ReadIndex + // [x0: .0, x1: .1, x2: .2, x3: .3, x4: .4, + // x5: .5, x6: .0, x7: .0, x8: .0, x9: .0, x10: .0] + + float out[10] = {}; + float* out_ptr = out; + uint32_t total_frames = 3; + + uint32_t rv = ringBuffer.ReadNoCopy( + [&out_ptr, &total_frames](const Span<const float>& aInBuffer) { + uint32_t inFramesUsed = + std::min<uint32_t>(total_frames, aInBuffer.Length()); + PodMove(out_ptr, aInBuffer.data(), inFramesUsed); + out_ptr += inFramesUsed; + total_frames -= inFramesUsed; + return inFramesUsed; + }); + // v ReadIndex + // [x0: .0, x1: .1, x2: .2, x3: .3, x4: .4, + // x5: .5, x6: .0, x7: .0, x8: .0, x9: .0, x10: .0] + EXPECT_EQ(rv, 3u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 7u); + EXPECT_EQ(ringBuffer.AvailableRead(), 3u); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_FLOAT_EQ(out[i], in[i]); + } + + total_frames = 3; + rv = ringBuffer.ReadNoCopy( + [&out_ptr, &total_frames](const Span<const float>& aInBuffer) { + uint32_t inFramesUsed = + std::min<uint32_t>(total_frames, aInBuffer.Length()); + PodMove(out_ptr, aInBuffer.data(), inFramesUsed); + out_ptr += inFramesUsed; + total_frames -= inFramesUsed; + return inFramesUsed; + }); + // [x0: .0, x1: .1, x2: .2, x3: .3, x4: .4, + // x5: .5, x6: .0, x7: .0, x8: .0, x9: .0, x10: .0] + // ^ ReadIndex + EXPECT_EQ(rv, 3u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_FLOAT_EQ(out[i + 3], in[i + 3]); + } + + ringBuffer.Write(Span(in, 8)); + // Now the buffer contains: + // [x0: .5, x1: .6, x2: .7, x3: .3, x4: .4, + // x5: .5, x6: .0, x7: .1, x8: .2, x9: .3, x10: .4 + // ^ ReadIndex + + // reset the pointer before lambdas reuse + out_ptr = out; + total_frames = 3; + rv = ringBuffer.ReadNoCopy( + [&out_ptr, &total_frames](const Span<const float>& aInBuffer) { + uint32_t inFramesUsed = + std::min<uint32_t>(total_frames, aInBuffer.Length()); + PodMove(out_ptr, aInBuffer.data(), inFramesUsed); + out_ptr += inFramesUsed; + total_frames -= inFramesUsed; + return inFramesUsed; + }); + // Now the buffer contains: + // [x0: .5, x1: .6, x2: .2, x3: .3, x4: .4, + // x5: .5, x6: .0, x7: .1, x8: .2, x9: .3, x10: .4 + // ^ ReadIndex + EXPECT_EQ(rv, 3u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 5u); + EXPECT_EQ(ringBuffer.AvailableRead(), 5u); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_FLOAT_EQ(out[i], in[i]); + } + + total_frames = 3; + rv = ringBuffer.ReadNoCopy( + [&out_ptr, &total_frames](const Span<const float>& aInBuffer) { + uint32_t inFramesUsed = + std::min<uint32_t>(total_frames, aInBuffer.Length()); + PodMove(out_ptr, aInBuffer.data(), inFramesUsed); + out_ptr += inFramesUsed; + total_frames -= inFramesUsed; + return inFramesUsed; + }); + // Now the buffer contains: + // v ReadIndex + // [x0: .5, x1: .6, x2: .7, x3: .3, x4: .4, + // x5: .5, x6: .0, x7: .1, x8: .2, x9: .3, x10: .4 + EXPECT_EQ(rv, 3u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 8u); + EXPECT_EQ(ringBuffer.AvailableRead(), 2u); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_FLOAT_EQ(out[i + 3], in[i + 3]); + } + + total_frames = 3; + rv = ringBuffer.ReadNoCopy( + [&out_ptr, &total_frames](const Span<const float>& aInBuffer) { + uint32_t inFramesUsed = + std::min<uint32_t>(total_frames, aInBuffer.Length()); + PodMove(out_ptr, aInBuffer.data(), inFramesUsed); + out_ptr += inFramesUsed; + total_frames -= inFramesUsed; + return inFramesUsed; + }); + // Now the buffer contains: + // v ReadIndex + // [x0: .5, x1: .6, x2: .7, x3: .3, x4: .4, + // x5: .5, x6: .0, x7: .1, x8: .2, x9: .3, x10: .4 + EXPECT_EQ(rv, 2u); + EXPECT_EQ(total_frames, 1u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_FLOAT_EQ(out[i + 6], in[i + 6]); + } +} + +TEST(TestAudioRingBuffer, NoCopyShort2) +{ + AudioRingBuffer ringBuffer(11 * sizeof(short)); + ringBuffer.SetSampleFormat(AUDIO_FORMAT_S16); + + short in[8] = {0, 1, 2, 3, 4, 5, 6, 7}; + ringBuffer.Write(Span(in, 6)); + // v ReadIndex + // [x0: 0, x1: 1, x2: 2, x3: 3, x4: 4, + // x5: 5, x6: 0, x7: 0, x8: 0, x9: 0, x10: 0] + + short out[10] = {}; + short* out_ptr = out; + uint32_t total_frames = 3; + + uint32_t rv = ringBuffer.ReadNoCopy( + [&out_ptr, &total_frames](const Span<const short>& aInBuffer) { + uint32_t inFramesUsed = + std::min<uint32_t>(total_frames, aInBuffer.Length()); + PodMove(out_ptr, aInBuffer.data(), inFramesUsed); + out_ptr += inFramesUsed; + total_frames -= inFramesUsed; + return inFramesUsed; + }); + // v ReadIndex + // [x0: 0, x1: 1, x2: 2, x3: 3, x4: 4, + // x5: 5, x6: 0, x7: 0, x8: 0, x9: 0, x10: 0] + EXPECT_EQ(rv, 3u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 7u); + EXPECT_EQ(ringBuffer.AvailableRead(), 3u); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_EQ(out[i], in[i]); + } + + total_frames = 3; + rv = ringBuffer.ReadNoCopy( + [&out_ptr, &total_frames](const Span<const short>& aInBuffer) { + uint32_t inFramesUsed = + std::min<uint32_t>(total_frames, aInBuffer.Length()); + PodMove(out_ptr, aInBuffer.data(), inFramesUsed); + out_ptr += inFramesUsed; + total_frames -= inFramesUsed; + return inFramesUsed; + }); + // [x0: 0, x1: 1, x2: 2, x3: 3, x4: 4, + // x5: 5, x6: 0, x7: 0, x8: 0, x9: 0, x10: .0] + // ^ ReadIndex + EXPECT_EQ(rv, 3u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_EQ(out[i + 3], in[i + 3]); + } + + ringBuffer.Write(Span(in, 8)); + // Now the buffer contains: + // [x0: 5, x1: 6, x2: 7, x3: 3, x4: 4, + // x5: 5, x6: 0, x7: 1, x8: 2, x9: 3, x10: 4 + // ^ ReadIndex + + // reset the pointer before lambdas reuse + out_ptr = out; + total_frames = 3; + rv = ringBuffer.ReadNoCopy( + [&out_ptr, &total_frames](const Span<const short>& aInBuffer) { + uint32_t inFramesUsed = + std::min<uint32_t>(total_frames, aInBuffer.Length()); + PodMove(out_ptr, aInBuffer.data(), inFramesUsed); + out_ptr += inFramesUsed; + total_frames -= inFramesUsed; + return inFramesUsed; + }); + // Now the buffer contains: + // [x0: 5, x1: 6, x2: 2, x3: 3, x4: 4, + // x5: 5, x6: 0, x7: 1, x8: 2, x9: 3, x10: 4 + // ^ ReadIndex + EXPECT_EQ(rv, 3u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 5u); + EXPECT_EQ(ringBuffer.AvailableRead(), 5u); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_EQ(out[i], in[i]); + } + + total_frames = 3; + rv = ringBuffer.ReadNoCopy( + [&out_ptr, &total_frames](const Span<const short>& aInBuffer) { + uint32_t inFramesUsed = + std::min<uint32_t>(total_frames, aInBuffer.Length()); + PodMove(out_ptr, aInBuffer.data(), inFramesUsed); + out_ptr += inFramesUsed; + total_frames -= inFramesUsed; + return inFramesUsed; + }); + // Now the buffer contains: + // v ReadIndex + // [x0: 5, x1: 6, x2: 7, x3: 3, x4: 4, + // x5: 5, x6: 0, x7: 1, x8: 2, x9: 3, x10: 4 + EXPECT_EQ(rv, 3u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 8u); + EXPECT_EQ(ringBuffer.AvailableRead(), 2u); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_EQ(out[i + 3], in[i + 3]); + } + + total_frames = 3; + rv = ringBuffer.ReadNoCopy( + [&out_ptr, &total_frames](const Span<const short>& aInBuffer) { + uint32_t inFramesUsed = + std::min<uint32_t>(total_frames, aInBuffer.Length()); + PodMove(out_ptr, aInBuffer.data(), inFramesUsed); + out_ptr += inFramesUsed; + total_frames -= inFramesUsed; + return inFramesUsed; + }); + // Now the buffer contains: + // v ReadIndex + // [x0: 5, x1: 6, x2: 7, x3: 3, x4: 4, + // x5: 5, x6: 0, x7: 1, x8: 2, x9: 3, x10: 4 + EXPECT_EQ(rv, 2u); + EXPECT_EQ(total_frames, 1u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_EQ(out[i + 6], in[i + 6]); + } +} + +TEST(TestAudioRingBuffer, DiscardFloat) +{ + AudioRingBuffer ringBuffer(11 * sizeof(float)); + ringBuffer.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + + float in[8] = {.0, .1, .2, .3, .4, .5, .6, .7}; + ringBuffer.Write(Span(in, 8)); + + uint32_t rv = ringBuffer.Discard(3); + EXPECT_EQ(rv, 3u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 5u); + EXPECT_EQ(ringBuffer.AvailableRead(), 5u); + + float out[8] = {}; + rv = ringBuffer.Read(Span(out, 3)); + EXPECT_EQ(rv, 3u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 8u); + EXPECT_EQ(ringBuffer.AvailableRead(), 2u); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_FLOAT_EQ(out[i], in[i + 3]); + } + + rv = ringBuffer.Discard(3); + EXPECT_EQ(rv, 2u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + + ringBuffer.WriteSilence(4); + rv = ringBuffer.Discard(6); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); +} + +TEST(TestAudioRingBuffer, DiscardShort) +{ + AudioRingBuffer ringBuffer(11 * sizeof(short)); + ringBuffer.SetSampleFormat(AUDIO_FORMAT_S16); + + short in[8] = {0, 1, 2, 3, 4, 5, 6, 7}; + ringBuffer.Write(Span(in, 8)); + + uint32_t rv = ringBuffer.Discard(3); + EXPECT_EQ(rv, 3u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 5u); + EXPECT_EQ(ringBuffer.AvailableRead(), 5u); + + short out[8] = {}; + rv = ringBuffer.Read(Span(out, 3)); + EXPECT_EQ(rv, 3u); + EXPECT_TRUE(!ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 8u); + EXPECT_EQ(ringBuffer.AvailableRead(), 2u); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_EQ(out[i], in[i + 3]); + } + + rv = ringBuffer.Discard(3); + EXPECT_EQ(rv, 2u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); + + ringBuffer.WriteSilence(4); + rv = ringBuffer.Discard(6); + EXPECT_EQ(rv, 4u); + EXPECT_TRUE(ringBuffer.IsEmpty()); + EXPECT_TRUE(!ringBuffer.IsFull()); + EXPECT_EQ(ringBuffer.AvailableWrite(), 10u); + EXPECT_EQ(ringBuffer.AvailableRead(), 0u); +} + +TEST(TestRingBuffer, WriteFromRing1) +{ + AudioRingBuffer ringBuffer1(11 * sizeof(float)); + ringBuffer1.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + AudioRingBuffer ringBuffer2(11 * sizeof(float)); + ringBuffer2.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + + float in[4] = {.1, .2, .3, .4}; + uint32_t rv = ringBuffer1.Write(Span<const float>(in, 4)); + EXPECT_EQ(rv, 4u); + + EXPECT_EQ(ringBuffer2.AvailableRead(), 0u); + rv = ringBuffer2.Write(ringBuffer1, 4); + EXPECT_EQ(rv, 4u); + EXPECT_EQ(ringBuffer2.AvailableRead(), 4u); + + float out[4] = {}; + rv = ringBuffer2.Read(Span<float>(out, 4)); + EXPECT_EQ(rv, 4u); + for (uint32_t i = 0; i < 4; ++i) { + EXPECT_FLOAT_EQ(in[i], out[i]); + } +} + +TEST(TestRingBuffer, WriteFromRing2) +{ + AudioRingBuffer ringBuffer1(11 * sizeof(float)); + ringBuffer1.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + AudioRingBuffer ringBuffer2(11 * sizeof(float)); + ringBuffer2.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + + // Advance the index + ringBuffer2.WriteSilence(8); + ringBuffer2.Clear(); + + float in[4] = {.1, .2, .3, .4}; + uint32_t rv = ringBuffer1.Write(Span<const float>(in, 4)); + EXPECT_EQ(rv, 4u); + rv = ringBuffer2.Write(ringBuffer1, 4); + EXPECT_EQ(rv, 4u); + EXPECT_EQ(ringBuffer2.AvailableRead(), 4u); + + float out[4] = {}; + rv = ringBuffer2.Read(Span<float>(out, 4)); + EXPECT_EQ(rv, 4u); + for (uint32_t i = 0; i < 4; ++i) { + EXPECT_FLOAT_EQ(in[i], out[i]); + } +} + +TEST(TestRingBuffer, WriteFromRing3) +{ + AudioRingBuffer ringBuffer1(11 * sizeof(float)); + ringBuffer1.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + AudioRingBuffer ringBuffer2(11 * sizeof(float)); + ringBuffer2.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + + // Advance the index + ringBuffer2.WriteSilence(8); + ringBuffer2.Clear(); + ringBuffer2.WriteSilence(4); + ringBuffer2.Clear(); + + float in[4] = {.1, .2, .3, .4}; + uint32_t rv = ringBuffer1.Write(Span<const float>(in, 4)); + EXPECT_EQ(rv, 4u); + rv = ringBuffer2.Write(ringBuffer1, 4); + EXPECT_EQ(rv, 4u); + EXPECT_EQ(ringBuffer2.AvailableRead(), 4u); + + float out[4] = {}; + rv = ringBuffer2.Read(Span<float>(out, 4)); + EXPECT_EQ(rv, 4u); + for (uint32_t i = 0; i < 4; ++i) { + EXPECT_FLOAT_EQ(in[i], out[i]); + } +} + +TEST(TestAudioRingBuffer, WriteFromRingShort) +{ + AudioRingBuffer ringBuffer1(11 * sizeof(short)); + ringBuffer1.SetSampleFormat(AUDIO_FORMAT_S16); + + short in[8] = {0, 1, 2, 3, 4, 5, 6, 7}; + uint32_t rv = ringBuffer1.Write(Span(in, 8)); + EXPECT_EQ(rv, 8u); + + AudioRingBuffer ringBuffer2(11 * sizeof(short)); + ringBuffer2.SetSampleFormat(AUDIO_FORMAT_S16); + + rv = ringBuffer2.Write(ringBuffer1, 4); + EXPECT_EQ(rv, 4u); + EXPECT_EQ(ringBuffer2.AvailableRead(), 4u); + EXPECT_EQ(ringBuffer1.AvailableRead(), 8u); + + short out[4] = {}; + rv = ringBuffer2.Read(Span(out, 4)); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_EQ(out[i], in[i]); + } + + rv = ringBuffer2.Write(ringBuffer1, 4); + EXPECT_EQ(rv, 4u); + EXPECT_EQ(ringBuffer2.AvailableRead(), 4u); + EXPECT_EQ(ringBuffer1.AvailableRead(), 8u); + + ringBuffer1.Discard(4); + rv = ringBuffer2.Write(ringBuffer1, 4); + EXPECT_EQ(rv, 4u); + EXPECT_EQ(ringBuffer2.AvailableRead(), 8u); + EXPECT_EQ(ringBuffer1.AvailableRead(), 4u); + + short out2[8] = {}; + rv = ringBuffer2.Read(Span(out2, 8)); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_EQ(out2[i], in[i]); + } +} + +TEST(TestAudioRingBuffer, WriteFromRingFloat) +{ + AudioRingBuffer ringBuffer1(11 * sizeof(float)); + ringBuffer1.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + + float in[8] = {.0, .1, .2, .3, .4, .5, .6, .7}; + uint32_t rv = ringBuffer1.Write(Span(in, 8)); + EXPECT_EQ(rv, 8u); + + AudioRingBuffer ringBuffer2(11 * sizeof(float)); + ringBuffer2.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + + rv = ringBuffer2.Write(ringBuffer1, 4); + EXPECT_EQ(rv, 4u); + EXPECT_EQ(ringBuffer2.AvailableRead(), 4u); + EXPECT_EQ(ringBuffer1.AvailableRead(), 8u); + + float out[4] = {}; + rv = ringBuffer2.Read(Span(out, 4)); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_FLOAT_EQ(out[i], in[i]); + } + + rv = ringBuffer2.Write(ringBuffer1, 4); + EXPECT_EQ(rv, 4u); + EXPECT_EQ(ringBuffer2.AvailableRead(), 4u); + EXPECT_EQ(ringBuffer1.AvailableRead(), 8u); + + ringBuffer1.Discard(4); + rv = ringBuffer2.Write(ringBuffer1, 4); + EXPECT_EQ(rv, 4u); + EXPECT_EQ(ringBuffer2.AvailableRead(), 8u); + EXPECT_EQ(ringBuffer1.AvailableRead(), 4u); + + float out2[8] = {}; + rv = ringBuffer2.Read(Span(out2, 8)); + for (uint32_t i = 0; i < rv; ++i) { + EXPECT_FLOAT_EQ(out2[i], in[i]); + } +} diff --git a/dom/media/gtest/TestAudioSegment.cpp b/dom/media/gtest/TestAudioSegment.cpp new file mode 100644 index 0000000000..a2f50fdb8d --- /dev/null +++ b/dom/media/gtest/TestAudioSegment.cpp @@ -0,0 +1,336 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AudioSegment.h" +#include <iostream> +#include "gtest/gtest.h" + +using namespace mozilla; + +namespace audio_segment { + +/* Helper function to give us the maximum and minimum value that don't clip, + * for a given sample format (integer or floating-point). */ +template <typename T> +T GetLowValue(); + +template <typename T> +T GetHighValue(); + +template <typename T> +T GetSilentValue(); + +template <> +float GetLowValue<float>() { + return -1.0; +} + +template <> +int16_t GetLowValue<short>() { + return -INT16_MAX; +} + +template <> +float GetHighValue<float>() { + return 1.0; +} + +template <> +int16_t GetHighValue<short>() { + return INT16_MAX; +} + +template <> +float GetSilentValue() { + return 0.0; +} + +template <> +int16_t GetSilentValue() { + return 0; +} + +// Get an array of planar audio buffers that has the inverse of the index of the +// channel (1-indexed) as samples. +template <typename T> +const T* const* GetPlanarChannelArray(size_t aChannels, size_t aSize) { + T** channels = new T*[aChannels]; + for (size_t c = 0; c < aChannels; c++) { + channels[c] = new T[aSize]; + for (size_t i = 0; i < aSize; i++) { + channels[c][i] = FloatToAudioSample<T>(1. / (c + 1)); + } + } + return channels; +} + +template <typename T> +void DeletePlanarChannelsArray(const T* const* aArrays, size_t aChannels) { + for (size_t channel = 0; channel < aChannels; channel++) { + delete[] aArrays[channel]; + } + delete[] aArrays; +} + +template <typename T> +T** GetPlanarArray(size_t aChannels, size_t aSize) { + T** channels = new T*[aChannels]; + for (size_t c = 0; c < aChannels; c++) { + channels[c] = new T[aSize]; + for (size_t i = 0; i < aSize; i++) { + channels[c][i] = 0.0f; + } + } + return channels; +} + +template <typename T> +void DeletePlanarArray(T** aArrays, size_t aChannels) { + for (size_t channel = 0; channel < aChannels; channel++) { + delete[] aArrays[channel]; + } + delete[] aArrays; +} + +// Get an array of audio samples that have the inverse of the index of the +// channel (1-indexed) as samples. +template <typename T> +const T* GetInterleavedChannelArray(size_t aChannels, size_t aSize) { + size_t sampleCount = aChannels * aSize; + T* samples = new T[sampleCount]; + for (size_t i = 0; i < sampleCount; i++) { + uint32_t channel = (i % aChannels) + 1; + samples[i] = FloatToAudioSample<T>(1. / channel); + } + return samples; +} + +template <typename T> +void DeleteInterleavedChannelArray(const T* aArray) { + delete[] aArray; +} + +bool FuzzyEqual(float aLhs, float aRhs) { return std::abs(aLhs - aRhs) < 0.01; } + +template <typename SrcT, typename DstT> +void TestInterleaveAndConvert() { + size_t arraySize = 1024; + size_t maxChannels = 8; // 7.1 + for (uint32_t channels = 1; channels < maxChannels; channels++) { + const SrcT* const* src = GetPlanarChannelArray<SrcT>(channels, arraySize); + DstT* dst = new DstT[channels * arraySize]; + + InterleaveAndConvertBuffer(src, arraySize, 1.0, channels, dst); + + uint32_t channelIndex = 0; + for (size_t i = 0; i < arraySize * channels; i++) { + ASSERT_TRUE(FuzzyEqual( + dst[i], FloatToAudioSample<DstT>(1. / (channelIndex + 1)))); + channelIndex++; + channelIndex %= channels; + } + + DeletePlanarChannelsArray(src, channels); + delete[] dst; + } +} + +template <typename SrcT, typename DstT> +void TestDeinterleaveAndConvert() { + size_t arraySize = 1024; + size_t maxChannels = 8; // 7.1 + for (uint32_t channels = 1; channels < maxChannels; channels++) { + const SrcT* src = GetInterleavedChannelArray<SrcT>(channels, arraySize); + DstT** dst = GetPlanarArray<DstT>(channels, arraySize); + + DeinterleaveAndConvertBuffer(src, arraySize, channels, dst); + + for (size_t channel = 0; channel < channels; channel++) { + for (size_t i = 0; i < arraySize; i++) { + ASSERT_TRUE(FuzzyEqual(dst[channel][i], + FloatToAudioSample<DstT>(1. / (channel + 1)))); + } + } + + DeleteInterleavedChannelArray(src); + DeletePlanarArray(dst, channels); + } +} + +uint8_t gSilence[4096] = {0}; + +template <typename T> +T* SilentChannel() { + return reinterpret_cast<T*>(gSilence); +} + +template <typename T> +void TestUpmixStereo() { + size_t arraySize = 1024; + nsTArray<T*> channels; + nsTArray<const T*> channelsptr; + + channels.SetLength(1); + channelsptr.SetLength(1); + + channels[0] = new T[arraySize]; + + for (size_t i = 0; i < arraySize; i++) { + channels[0][i] = GetHighValue<T>(); + } + channelsptr[0] = channels[0]; + + AudioChannelsUpMix(&channelsptr, 2, SilentChannel<T>()); + + for (size_t channel = 0; channel < 2; channel++) { + for (size_t i = 0; i < arraySize; i++) { + ASSERT_TRUE(channelsptr[channel][i] == GetHighValue<T>()); + } + } + delete[] channels[0]; +} + +template <typename T> +void TestDownmixStereo() { + const size_t arraySize = 1024; + nsTArray<const T*> inputptr; + nsTArray<T*> input; + T** output; + + output = new T*[1]; + output[0] = new T[arraySize]; + + input.SetLength(2); + inputptr.SetLength(2); + + for (size_t channel = 0; channel < input.Length(); channel++) { + input[channel] = new T[arraySize]; + for (size_t i = 0; i < arraySize; i++) { + input[channel][i] = channel == 0 ? GetLowValue<T>() : GetHighValue<T>(); + } + inputptr[channel] = input[channel]; + } + + AudioChannelsDownMix(inputptr, output, 1, arraySize); + + for (size_t i = 0; i < arraySize; i++) { + ASSERT_TRUE(output[0][i] == GetSilentValue<T>()); + ASSERT_TRUE(output[0][i] == GetSilentValue<T>()); + } + + delete[] output[0]; + delete[] output; +} + +TEST(AudioSegment, Test) +{ + TestInterleaveAndConvert<float, float>(); + TestInterleaveAndConvert<float, int16_t>(); + TestInterleaveAndConvert<int16_t, float>(); + TestInterleaveAndConvert<int16_t, int16_t>(); + TestDeinterleaveAndConvert<float, float>(); + TestDeinterleaveAndConvert<float, int16_t>(); + TestDeinterleaveAndConvert<int16_t, float>(); + TestDeinterleaveAndConvert<int16_t, int16_t>(); + TestUpmixStereo<float>(); + TestUpmixStereo<int16_t>(); + TestDownmixStereo<float>(); + TestDownmixStereo<int16_t>(); +} + +template <class T, uint32_t Channels> +void fillChunk(AudioChunk* aChunk, int aDuration) { + static_assert(Channels != 0, "Filling 0 channels is a no-op"); + + aChunk->mDuration = aDuration; + + AutoTArray<nsTArray<T>, Channels> buffer; + buffer.SetLength(Channels); + aChunk->mChannelData.ClearAndRetainStorage(); + aChunk->mChannelData.SetCapacity(Channels); + for (nsTArray<T>& channel : buffer) { + T* ch = channel.AppendElements(aDuration); + for (int i = 0; i < aDuration; ++i) { + ch[i] = GetHighValue<T>(); + } + aChunk->mChannelData.AppendElement(ch); + } + + aChunk->mBuffer = new mozilla::SharedChannelArrayBuffer<T>(std::move(buffer)); + aChunk->mBufferFormat = AudioSampleTypeToFormat<T>::Format; +} + +TEST(AudioSegment, FlushAfter_ZeroDuration) +{ + AudioChunk c; + fillChunk<float, 2>(&c, 10); + + AudioSegment s; + s.AppendAndConsumeChunk(&c); + s.FlushAfter(0); + EXPECT_EQ(s.GetDuration(), 0); +} + +TEST(AudioSegment, FlushAfter_SmallerDuration) +{ + // It was crashing when the first chunk was silence (null) and FlushAfter + // was called for a duration, smaller or equal to the duration of the + // first chunk. + TrackTime duration = 10; + TrackTime smaller_duration = 8; + AudioChunk c1; + c1.SetNull(duration); + AudioChunk c2; + fillChunk<float, 2>(&c2, duration); + + AudioSegment s; + s.AppendAndConsumeChunk(&c1); + s.AppendAndConsumeChunk(&c2); + s.FlushAfter(smaller_duration); + EXPECT_EQ(s.GetDuration(), smaller_duration) << "Check new duration"; + + TrackTime chunkByChunkDuration = 0; + for (AudioSegment::ChunkIterator iter(s); !iter.IsEnded(); iter.Next()) { + chunkByChunkDuration += iter->GetDuration(); + } + EXPECT_EQ(s.GetDuration(), chunkByChunkDuration) + << "Confirm duration chunk by chunk"; +} + +TEST(AudioSegment, MemoizedOutputChannelCount) +{ + AudioSegment s; + EXPECT_EQ(s.MaxChannelCount(), 0U) << "0 channels on init"; + + s.AppendNullData(1); + EXPECT_EQ(s.MaxChannelCount(), 0U) << "Null data has 0 channels"; + + s.Clear(); + EXPECT_EQ(s.MaxChannelCount(), 0U) << "Still 0 after clearing"; + + AudioChunk c; + fillChunk<float, 1>(&c, 1); + s.AppendAndConsumeChunk(&c); + EXPECT_EQ(s.MaxChannelCount(), 1U) << "A single chunk's channel count"; + + fillChunk<float, 2>(&c, 1); + s.AppendAndConsumeChunk(&c); + EXPECT_EQ(s.MaxChannelCount(), 2U) << "The max of two chunks' channel count"; + + s.ForgetUpTo(2); + EXPECT_EQ(s.MaxChannelCount(), 2U) << "Memoized value with null chunks"; + + s.Clear(); + EXPECT_EQ(s.MaxChannelCount(), 2U) << "Still memoized after clearing"; + + fillChunk<float, 1>(&c, 1); + s.AppendAndConsumeChunk(&c); + EXPECT_EQ(s.MaxChannelCount(), 1U) << "Real chunk trumps memoized value"; + + s.Clear(); + EXPECT_EQ(s.MaxChannelCount(), 1U) << "Memoized value was updated"; +} + +} // namespace audio_segment diff --git a/dom/media/gtest/TestAudioTrackEncoder.cpp b/dom/media/gtest/TestAudioTrackEncoder.cpp new file mode 100644 index 0000000000..cd62d748f9 --- /dev/null +++ b/dom/media/gtest/TestAudioTrackEncoder.cpp @@ -0,0 +1,282 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "OpusTrackEncoder.h" + +#include "AudioGenerator.h" +#include "AudioSampleFormat.h" + +using namespace mozilla; + +class TestOpusTrackEncoder : public OpusTrackEncoder { + public: + explicit TestOpusTrackEncoder(TrackRate aTrackRate) + : OpusTrackEncoder(aTrackRate) {} + + // Return true if it has successfully initialized the Opus encoder. + bool TestOpusRawCreation(int aChannels) { + if (Init(aChannels) == NS_OK) { + if (IsInitialized()) { + return true; + } + } + return false; + } +}; + +static bool TestOpusInit(int aChannels, TrackRate aSamplingRate) { + TestOpusTrackEncoder encoder(aSamplingRate); + return encoder.TestOpusRawCreation(aChannels); +} + +TEST(OpusAudioTrackEncoder, InitRaw) +{ + // Expect false with 0 or negative channels of input signal. + EXPECT_FALSE(TestOpusInit(0, 16000)); + EXPECT_FALSE(TestOpusInit(-1, 16000)); + + // The Opus format supports up to 8 channels, and supports multitrack audio up + // to 255 channels, but the current implementation supports only mono and + // stereo, and downmixes any more than that. + // Expect false with channels of input signal exceed the max supported number. + EXPECT_FALSE(TestOpusInit(8 + 1, 16000)); + + // Should accept channels within valid range. + for (int i = 1; i <= 8; i++) { + EXPECT_TRUE(TestOpusInit(i, 16000)); + } + + // Expect false with 0 or negative sampling rate of input signal. + EXPECT_FALSE(TestOpusInit(1, 0)); + EXPECT_FALSE(TestOpusInit(1, -1)); + + // Verify sample rate bounds checking. + EXPECT_FALSE(TestOpusInit(2, 2000)); + EXPECT_FALSE(TestOpusInit(2, 4000)); + EXPECT_FALSE(TestOpusInit(2, 7999)); + EXPECT_TRUE(TestOpusInit(2, 8000)); + EXPECT_TRUE(TestOpusInit(2, 192000)); + EXPECT_FALSE(TestOpusInit(2, 192001)); + EXPECT_FALSE(TestOpusInit(2, 200000)); +} + +TEST(OpusAudioTrackEncoder, Init) +{ + { + // The encoder does not normally recieve enough info from null data to + // init. However, multiple attempts to do so, with sufficiently long + // duration segments, should result in a default-init. The first attempt + // should never do this though, even if the duration is long: + OpusTrackEncoder encoder(48000); + AudioSegment segment; + segment.AppendNullData(48000 * 100); + encoder.TryInit(segment, segment.GetDuration()); + EXPECT_FALSE(encoder.IsInitialized()); + + // Multiple init attempts should result in best effort init: + encoder.TryInit(segment, segment.GetDuration()); + EXPECT_TRUE(encoder.IsInitialized()); + } + + { + // For non-null segments we should init immediately + OpusTrackEncoder encoder(48000); + AudioSegment segment; + AudioGenerator<AudioDataValue> generator(2, 48000); + generator.Generate(segment, 1); + encoder.TryInit(segment, segment.GetDuration()); + EXPECT_TRUE(encoder.IsInitialized()); + } + + { + // Test low sample rate bound + OpusTrackEncoder encoder(7999); + AudioSegment segment; + AudioGenerator<AudioDataValue> generator(2, 7999); + generator.Generate(segment, 1); + encoder.TryInit(segment, segment.GetDuration()); + EXPECT_FALSE(encoder.IsInitialized()); + } + + { + // Test low sample rate bound + OpusTrackEncoder encoder(8000); + AudioSegment segment; + AudioGenerator<AudioDataValue> generator(2, 8000); + generator.Generate(segment, 1); + encoder.TryInit(segment, segment.GetDuration()); + EXPECT_TRUE(encoder.IsInitialized()); + } + + { + // Test high sample rate bound + OpusTrackEncoder encoder(192001); + AudioSegment segment; + AudioGenerator<AudioDataValue> generator(2, 192001); + generator.Generate(segment, 1); + encoder.TryInit(segment, segment.GetDuration()); + EXPECT_FALSE(encoder.IsInitialized()); + } + + { + // Test high sample rate bound + OpusTrackEncoder encoder(192000); + AudioSegment segment; + AudioGenerator<AudioDataValue> generator(2, 192000); + generator.Generate(segment, 1); + encoder.TryInit(segment, segment.GetDuration()); + EXPECT_TRUE(encoder.IsInitialized()); + } + + { + // Test that it takes 10s to trigger default-init. + OpusTrackEncoder encoder(48000); + AudioSegment longSegment; + longSegment.AppendNullData(48000 * 10 - 1); + AudioSegment shortSegment; + shortSegment.AppendNullData(1); + encoder.TryInit(longSegment, longSegment.GetDuration()); + EXPECT_FALSE(encoder.IsInitialized()); + encoder.TryInit(shortSegment, shortSegment.GetDuration()); + EXPECT_FALSE(encoder.IsInitialized()); + encoder.TryInit(shortSegment, shortSegment.GetDuration()); + EXPECT_TRUE(encoder.IsInitialized()); + } +} + +static int TestOpusResampler(TrackRate aSamplingRate) { + OpusTrackEncoder encoder(aSamplingRate); + return encoder.mOutputSampleRate; +} + +TEST(OpusAudioTrackEncoder, Resample) +{ + // Sampling rates of data to be fed to Opus encoder, should remain unchanged + // if it is one of Opus supported rates (8000, 12000, 16000, 24000 and 48000 + // (kHz)) at initialization. + EXPECT_TRUE(TestOpusResampler(8000) == 8000); + EXPECT_TRUE(TestOpusResampler(12000) == 12000); + EXPECT_TRUE(TestOpusResampler(16000) == 16000); + EXPECT_TRUE(TestOpusResampler(24000) == 24000); + EXPECT_TRUE(TestOpusResampler(48000) == 48000); + + // Otherwise, it should be resampled to 48kHz by resampler. + EXPECT_TRUE(TestOpusResampler(9600) == 48000); + EXPECT_TRUE(TestOpusResampler(44100) == 48000); +} + +TEST(OpusAudioTrackEncoder, FetchMetadata) +{ + const int32_t channels = 1; + const TrackRate sampleRate = 44100; + TestOpusTrackEncoder encoder(sampleRate); + EXPECT_TRUE(encoder.TestOpusRawCreation(channels)); + + RefPtr<TrackMetadataBase> metadata = encoder.GetMetadata(); + ASSERT_EQ(TrackMetadataBase::METADATA_OPUS, metadata->GetKind()); + + RefPtr<OpusMetadata> opusMeta = static_cast<OpusMetadata*>(metadata.get()); + EXPECT_EQ(channels, opusMeta->mChannels); + EXPECT_EQ(sampleRate, opusMeta->mSamplingFrequency); +} + +TEST(OpusAudioTrackEncoder, FrameEncode) +{ + const int32_t channels = 1; + const TrackRate sampleRate = 44100; + TestOpusTrackEncoder encoder(sampleRate); + EXPECT_TRUE(encoder.TestOpusRawCreation(channels)); + + // Generate five seconds of raw audio data. + AudioGenerator<AudioDataValue> generator(channels, sampleRate); + AudioSegment segment; + const int32_t samples = sampleRate * 5; + generator.Generate(segment, samples); + + encoder.AppendAudioSegment(std::move(segment)); + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + EXPECT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + // Verify that encoded data is 5 seconds long. + uint64_t totalDuration = 0; + for (auto& frame : frames) { + totalDuration += frame->mDuration; + } + // 44100 as used above gets resampled to 48000 for opus. + const uint64_t five = 48000 * 5; + EXPECT_EQ(five + encoder.GetLookahead(), totalDuration); +} + +TEST(OpusAudioTrackEncoder, DefaultInitDuration) +{ + const TrackRate rate = 44100; + OpusTrackEncoder encoder(rate); + AudioGenerator<AudioDataValue> generator(2, rate); + AudioSegment segment; + // 15 seconds should trigger the default-init rate. + // The default-init timeout is evaluated once per chunk, so keep chunks + // reasonably short. + for (int i = 0; i < 150; ++i) { + generator.Generate(segment, rate / 10); + } + encoder.AppendAudioSegment(std::move(segment)); + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + EXPECT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + // Verify that encoded data is 15 seconds long. + uint64_t totalDuration = 0; + for (auto& frame : frames) { + totalDuration += frame->mDuration; + } + // 44100 as used above gets resampled to 48000 for opus. + const uint64_t fifteen = 48000 * 15; + EXPECT_EQ(totalDuration, fifteen + encoder.GetLookahead()); +} + +uint64_t TestSampleRate(TrackRate aSampleRate, uint64_t aInputFrames) { + OpusTrackEncoder encoder(aSampleRate); + AudioGenerator<AudioDataValue> generator(2, aSampleRate); + AudioSegment segment; + const uint64_t chunkSize = aSampleRate / 10; + const uint64_t chunks = aInputFrames / chunkSize; + // 15 seconds should trigger the default-init rate. + // The default-init timeout is evaluated once per chunk, so keep chunks + // reasonably short. + for (size_t i = 0; i < chunks; ++i) { + generator.Generate(segment, chunkSize); + } + generator.Generate(segment, aInputFrames % chunks); + encoder.AppendAudioSegment(std::move(segment)); + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + EXPECT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + // Verify that encoded data is 15 seconds long. + uint64_t totalDuration = 0; + for (auto& frame : frames) { + totalDuration += frame->mDuration; + } + return totalDuration - encoder.GetLookahead(); +} + +TEST(OpusAudioTrackEncoder, DurationSampleRates) +{ + // Factors of 48k + EXPECT_EQ(TestSampleRate(48000, 48000 * 3 / 2), 48000U * 3 / 2); + EXPECT_EQ(TestSampleRate(24000, 24000 * 3 / 2), 48000U * 3 / 2); + EXPECT_EQ(TestSampleRate(16000, 16000 * 3 / 2), 48000U * 3 / 2); + EXPECT_EQ(TestSampleRate(12000, 12000 * 3 / 2), 48000U * 3 / 2); + EXPECT_EQ(TestSampleRate(8000, 8000 * 3 / 2), 48000U * 3 / 2); + + // Non-factors of 48k, resampled + EXPECT_EQ(TestSampleRate(44100, 44100 * 3 / 2), 48000U * 3 / 2); + EXPECT_EQ(TestSampleRate(32000, 32000 * 3 / 2), 48000U * 3 / 2); + EXPECT_EQ(TestSampleRate(96000, 96000 * 3 / 2), 48000U * 3 / 2); + EXPECT_EQ(TestSampleRate(33330, 33330 * 3 / 2), 48000U * 3 / 2); +} diff --git a/dom/media/gtest/TestAudioTrackGraph.cpp b/dom/media/gtest/TestAudioTrackGraph.cpp new file mode 100644 index 0000000000..f5dbbb7566 --- /dev/null +++ b/dom/media/gtest/TestAudioTrackGraph.cpp @@ -0,0 +1,774 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MediaTrackGraphImpl.h" + +#include "gmock/gmock.h" +#include "gtest/gtest-printers.h" +#include "gtest/gtest.h" + +#include "CrossGraphPort.h" +#ifdef MOZ_WEBRTC +# include "MediaEngineWebRTCAudio.h" +#endif // MOZ_WEBRTC +#include "MockCubeb.h" +#include "mozilla/Preferences.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "WaitFor.h" +#include "WavDumper.h" + +#define DRIFT_BUFFERING_PREF "media.clockdrift.buffering" + +using namespace mozilla; + +namespace { +// Short-hand for InvokeAsync on the current thread. +#define Invoke(f) InvokeAsync(GetCurrentSerialEventTarget(), __func__, f) + +// Short-hand for DispatchToCurrentThread with a function. +#define DispatchFunction(f) \ + NS_DispatchToCurrentThread(NS_NewRunnableFunction(__func__, f)) + +// Short-hand for DispatchToCurrentThread with a method with arguments +#define DispatchMethod(t, m, args...) \ + NS_DispatchToCurrentThread(NewRunnableMethod(__func__, t, m, ##args)) + +#ifdef MOZ_WEBRTC +/* + * Common ControlMessages + */ +struct StartInputProcessing : public ControlMessage { + const RefPtr<AudioInputTrack> mInputTrack; + const RefPtr<AudioInputProcessing> mInputProcessing; + + StartInputProcessing(AudioInputTrack* aTrack, + AudioInputProcessing* aInputProcessing) + : ControlMessage(aTrack), + mInputTrack(aTrack), + mInputProcessing(aInputProcessing) {} + void Run() override { mInputProcessing->Start(); } +}; + +struct StopInputProcessing : public ControlMessage { + const RefPtr<AudioInputProcessing> mInputProcessing; + + explicit StopInputProcessing(AudioInputProcessing* aInputProcessing) + : ControlMessage(nullptr), mInputProcessing(aInputProcessing) {} + void Run() override { mInputProcessing->Stop(); } +}; + +struct SetPassThrough : public ControlMessage { + const RefPtr<AudioInputProcessing> mInputProcessing; + const bool mPassThrough; + + SetPassThrough(MediaTrack* aTrack, AudioInputProcessing* aInputProcessing, + bool aPassThrough) + : ControlMessage(aTrack), + mInputProcessing(aInputProcessing), + mPassThrough(aPassThrough) {} + void Run() override { + EXPECT_EQ(mInputProcessing->PassThrough(mTrack->GraphImpl()), + !mPassThrough); + mInputProcessing->SetPassThrough(mTrack->GraphImpl(), mPassThrough); + } +}; +#endif // MOZ_WEBRTC + +class GoFaster : public ControlMessage { + MockCubeb* mCubeb; + + public: + explicit GoFaster(MockCubeb* aCubeb) + : ControlMessage(nullptr), mCubeb(aCubeb) {} + void Run() override { mCubeb->GoFaster(); } +}; + +} // namespace + +/* + * The set of tests here are a bit special. In part because they're async and + * depends on the graph thread to function. In part because they depend on main + * thread stable state to send messages to the graph. + * + * Any message sent from the main thread to the graph through the graph's + * various APIs are scheduled to run in stable state. Stable state occurs after + * a task in the main thread eventloop has run to completion. + * + * Since gtests are generally sync and on main thread, calling into the graph + * may schedule a stable state runnable but with no task in the eventloop to + * trigger stable state. Therefore care must be taken to always call into the + * graph from a task, typically via InvokeAsync or a dispatch to main thread. + */ + +TEST(TestAudioTrackGraph, DifferentDeviceIDs) +{ + MockCubeb* cubeb = new MockCubeb(); + CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); + + MediaTrackGraph* g1 = MediaTrackGraph::GetInstance( + MediaTrackGraph::AUDIO_THREAD_DRIVER, /*window*/ nullptr, + MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, + /*OutputDeviceID*/ nullptr); + + MediaTrackGraph* g2 = MediaTrackGraph::GetInstance( + MediaTrackGraph::AUDIO_THREAD_DRIVER, /*window*/ nullptr, + MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, + /*OutputDeviceID*/ reinterpret_cast<cubeb_devid>(1)); + + MediaTrackGraph* g1_2 = MediaTrackGraph::GetInstance( + MediaTrackGraph::AUDIO_THREAD_DRIVER, /*window*/ nullptr, + MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, + /*OutputDeviceID*/ nullptr); + + MediaTrackGraph* g2_2 = MediaTrackGraph::GetInstance( + MediaTrackGraph::AUDIO_THREAD_DRIVER, /*window*/ nullptr, + MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, + /*OutputDeviceID*/ reinterpret_cast<cubeb_devid>(1)); + + EXPECT_NE(g1, g2) << "Different graphs due to different device ids"; + EXPECT_EQ(g1, g1_2) << "Same graphs for same device ids"; + EXPECT_EQ(g2, g2_2) << "Same graphs for same device ids"; + + for (MediaTrackGraph* g : {g1, g2}) { + // Dummy track to make graph rolling. Add it and remove it to remove the + // graph from the global hash table and let it shutdown. + + using SourceTrackPromise = MozPromise<SourceMediaTrack*, nsresult, true>; + auto p = Invoke([g] { + return SourceTrackPromise::CreateAndResolve( + g->CreateSourceTrack(MediaSegment::AUDIO), __func__); + }); + + WaitFor(cubeb->StreamInitEvent()); + RefPtr<SourceMediaTrack> dummySource = WaitFor(p).unwrap(); + + DispatchMethod(dummySource, &SourceMediaTrack::Destroy); + + WaitFor(cubeb->StreamDestroyEvent()); + } +} + +TEST(TestAudioTrackGraph, SetOutputDeviceID) +{ + MockCubeb* cubeb = new MockCubeb(); + CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); + + // Set the output device id in GetInstance method confirm that it is the one + // used in cubeb_stream_init. + MediaTrackGraph* graph = MediaTrackGraph::GetInstance( + MediaTrackGraph::AUDIO_THREAD_DRIVER, /*window*/ nullptr, + MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, + /*OutputDeviceID*/ reinterpret_cast<cubeb_devid>(2)); + + // Dummy track to make graph rolling. Add it and remove it to remove the + // graph from the global hash table and let it shutdown. + RefPtr<SourceMediaTrack> dummySource; + DispatchFunction( + [&] { dummySource = graph->CreateSourceTrack(MediaSegment::AUDIO); }); + + RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent()); + + EXPECT_EQ(stream->GetOutputDeviceID(), reinterpret_cast<cubeb_devid>(2)) + << "After init confirm the expected output device id"; + + // Test has finished, destroy the track to shutdown the MTG. + DispatchMethod(dummySource, &SourceMediaTrack::Destroy); + WaitFor(cubeb->StreamDestroyEvent()); +} + +TEST(TestAudioTrackGraph, NotifyDeviceStarted) +{ + MockCubeb* cubeb = new MockCubeb(); + CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); + + MediaTrackGraph* graph = MediaTrackGraph::GetInstance( + MediaTrackGraph::AUDIO_THREAD_DRIVER, /*window*/ nullptr, + MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr); + + RefPtr<SourceMediaTrack> dummySource; + Unused << WaitFor(Invoke([&] { + // Dummy track to make graph rolling. Add it and remove it to remove the + // graph from the global hash table and let it shutdown. + dummySource = graph->CreateSourceTrack(MediaSegment::AUDIO); + + return graph->NotifyWhenDeviceStarted(dummySource); + })); + + { + MediaTrackGraphImpl* graph = dummySource->GraphImpl(); + MonitorAutoLock lock(graph->GetMonitor()); + EXPECT_TRUE(graph->CurrentDriver()->AsAudioCallbackDriver()); + EXPECT_TRUE(graph->CurrentDriver()->ThreadRunning()); + } + + // Test has finished, destroy the track to shutdown the MTG. + DispatchMethod(dummySource, &SourceMediaTrack::Destroy); + WaitFor(cubeb->StreamDestroyEvent()); +} + +#ifdef MOZ_WEBRTC +TEST(TestAudioTrackGraph, ErrorCallback) +{ + MockCubeb* cubeb = new MockCubeb(); + CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); + + MediaTrackGraph* graph = MediaTrackGraph::GetInstance( + MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*window*/ nullptr, + MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr); + + // Dummy track to make graph rolling. Add it and remove it to remove the + // graph from the global hash table and let it shutdown. + // + // We open an input through this track so that there's something triggering + // EnsureNextIteration on the fallback driver after the callback driver has + // gotten the error. + RefPtr<AudioInputTrack> inputTrack; + RefPtr<AudioInputProcessing> listener; + auto started = Invoke([&] { + inputTrack = AudioInputTrack::Create(graph); + listener = new AudioInputProcessing(2, PRINCIPAL_HANDLE_NONE); + inputTrack->GraphImpl()->AppendMessage( + MakeUnique<SetPassThrough>(inputTrack, listener, true)); + inputTrack->SetInputProcessing(listener); + inputTrack->GraphImpl()->AppendMessage( + MakeUnique<StartInputProcessing>(inputTrack, listener)); + inputTrack->OpenAudioInput((void*)1, listener); + return graph->NotifyWhenDeviceStarted(inputTrack); + }); + + RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent()); + Result<bool, nsresult> rv = WaitFor(started); + EXPECT_TRUE(rv.unwrapOr(false)); + + // Force a cubeb state_callback error and see that we don't crash. + DispatchFunction([&] { stream->ForceError(); }); + + // Wait for both the error to take effect, and the driver to restart. + bool errored = false, init = false; + MediaEventListener errorListener = stream->ErrorForcedEvent().Connect( + AbstractThread::GetCurrent(), [&] { errored = true; }); + MediaEventListener initListener = cubeb->StreamInitEvent().Connect( + AbstractThread::GetCurrent(), [&] { init = true; }); + SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>( + [&] { return errored && init; }); + errorListener.Disconnect(); + initListener.Disconnect(); + + // Clean up. + DispatchFunction([&] { + inputTrack->GraphImpl()->AppendMessage( + MakeUnique<StopInputProcessing>(listener)); + Maybe<CubebUtils::AudioDeviceID> id = + Some(reinterpret_cast<CubebUtils::AudioDeviceID>(1)); + inputTrack->CloseAudioInput(id); + inputTrack->Destroy(); + }); + WaitFor(cubeb->StreamDestroyEvent()); +} + +TEST(TestAudioTrackGraph, AudioInputTrack) +{ + MockCubeb* cubeb = new MockCubeb(); + CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); + auto unforcer = WaitFor(cubeb->ForceAudioThread()).unwrap(); + Unused << unforcer; + + // Start on a system clock driver, then switch to full-duplex in one go. If we + // did output-then-full-duplex we'd risk a second NotifyWhenDeviceStarted + // resolving early after checking the first audio driver only. + MediaTrackGraph* graph = MediaTrackGraph::GetInstance( + MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*window*/ nullptr, + MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr); + + RefPtr<AudioInputTrack> inputTrack; + RefPtr<ProcessedMediaTrack> outputTrack; + RefPtr<MediaInputPort> port; + RefPtr<AudioInputProcessing> listener; + auto p = Invoke([&] { + inputTrack = AudioInputTrack::Create(graph); + outputTrack = graph->CreateForwardedInputTrack(MediaSegment::AUDIO); + outputTrack->QueueSetAutoend(false); + outputTrack->AddAudioOutput(reinterpret_cast<void*>(1)); + port = outputTrack->AllocateInputPort(inputTrack); + /* Primary graph: Open Audio Input through SourceMediaTrack */ + listener = new AudioInputProcessing(2, PRINCIPAL_HANDLE_NONE); + inputTrack->GraphImpl()->AppendMessage( + MakeUnique<SetPassThrough>(inputTrack, listener, true)); + inputTrack->SetInputProcessing(listener); + inputTrack->GraphImpl()->AppendMessage( + MakeUnique<StartInputProcessing>(inputTrack, listener)); + // Device id does not matter. Ignore. + inputTrack->OpenAudioInput((void*)1, listener); + return graph->NotifyWhenDeviceStarted(inputTrack); + }); + + RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent()); + EXPECT_TRUE(stream->mHasInput); + Unused << WaitFor(p); + + // Wait for a second worth of audio data. GoFaster is dispatched through a + // ControlMessage so that it is called in the first audio driver iteration. + // Otherwise the audio driver might be going very fast while the fallback + // system clock driver is still in an iteration. + DispatchFunction([&] { + inputTrack->GraphImpl()->AppendMessage(MakeUnique<GoFaster>(cubeb)); + }); + uint32_t totalFrames = 0; + WaitUntil(stream->FramesVerifiedEvent(), [&](uint32_t aFrames) { + totalFrames += aFrames; + return totalFrames > static_cast<uint32_t>(graph->GraphRate()); + }); + cubeb->DontGoFaster(); + + // Clean up. + DispatchFunction([&] { + outputTrack->RemoveAudioOutput((void*)1); + outputTrack->Destroy(); + port->Destroy(); + inputTrack->GraphImpl()->AppendMessage( + MakeUnique<StopInputProcessing>(listener)); + Maybe<CubebUtils::AudioDeviceID> id = + Some(reinterpret_cast<CubebUtils::AudioDeviceID>(1)); + inputTrack->CloseAudioInput(id); + inputTrack->Destroy(); + }); + + uint32_t inputRate = stream->InputSampleRate(); + uint32_t inputFrequency = stream->InputFrequency(); + uint64_t preSilenceSamples; + uint32_t estimatedFreq; + uint32_t nrDiscontinuities; + Tie(preSilenceSamples, estimatedFreq, nrDiscontinuities) = + WaitFor(stream->OutputVerificationEvent()); + + EXPECT_EQ(estimatedFreq, inputFrequency); + std::cerr << "PreSilence: " << preSilenceSamples << std::endl; + // We buffer 128 frames in passthrough mode. See AudioInputProcessing::Pull. + EXPECT_GE(preSilenceSamples, 128U); + // If the fallback system clock driver is doing a graph iteration before the + // first audio driver iteration comes in, that iteration is ignored and + // results in zeros. It takes one fallback driver iteration *after* the audio + // driver has started to complete the switch, *usually* resulting two + // 10ms-iterations of silence; sometimes only one. + EXPECT_LE(preSilenceSamples, 128U + 2 * inputRate / 100 /* 2*10ms */); + // The waveform from AudioGenerator starts at 0, but we don't control its + // ending, so we expect a discontinuity there. + EXPECT_LE(nrDiscontinuities, 1U); +} + +TEST(TestAudioTrackGraph, ReOpenAudioInput) +{ + MockCubeb* cubeb = new MockCubeb(); + CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); + + // 48k is a native processing rate, and avoids a resampling pass compared + // to 44.1k. The resampler may add take a few frames to stabilize, which show + // as unexected discontinuities in the test. + const TrackRate rate = 48000; + + MediaTrackGraph* graph = MediaTrackGraph::GetInstance( + MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*window*/ nullptr, rate, nullptr); + + RefPtr<AudioInputTrack> inputTrack; + RefPtr<ProcessedMediaTrack> outputTrack; + RefPtr<MediaInputPort> port; + RefPtr<AudioInputProcessing> listener; + auto p = Invoke([&] { + inputTrack = AudioInputTrack::Create(graph); + outputTrack = graph->CreateForwardedInputTrack(MediaSegment::AUDIO); + outputTrack->QueueSetAutoend(false); + outputTrack->AddAudioOutput(reinterpret_cast<void*>(1)); + port = outputTrack->AllocateInputPort(inputTrack); + listener = new AudioInputProcessing(2, PRINCIPAL_HANDLE_NONE); + inputTrack->SetInputProcessing(listener); + inputTrack->GraphImpl()->AppendMessage( + MakeUnique<StartInputProcessing>(inputTrack, listener)); + inputTrack->OpenAudioInput((void*)1, listener); + return graph->NotifyWhenDeviceStarted(inputTrack); + }); + + RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent()); + EXPECT_TRUE(stream->mHasInput); + Unused << WaitFor(p); + + // Set a drift factor so that we don't dont produce perfect 10ms-chunks. This + // will exercise whatever buffers are in the audio processing pipeline, and + // the bookkeeping surrounding them. + stream->SetDriftFactor(1.111); + + // Wait for a second worth of audio data. GoFaster is dispatched through a + // ControlMessage so that it is called in the first audio driver iteration. + // Otherwise the audio driver might be going very fast while the fallback + // system clock driver is still in an iteration. + DispatchFunction([&] { + inputTrack->GraphImpl()->AppendMessage(MakeUnique<GoFaster>(cubeb)); + }); + { + uint32_t totalFrames = 0; + WaitUntil(stream->FramesProcessedEvent(), [&](uint32_t aFrames) { + totalFrames += aFrames; + return totalFrames > static_cast<uint32_t>(graph->GraphRate()); + }); + } + cubeb->DontGoFaster(); + + // Close the input to see that no asserts go off due to bad state. + DispatchFunction([&] { + // Device id does not matter. Ignore. + auto id = Some((CubebUtils::AudioDeviceID)1); + inputTrack->CloseAudioInput(id); + }); + + stream = WaitFor(cubeb->StreamInitEvent()); + EXPECT_FALSE(stream->mHasInput); + Unused << WaitFor( + Invoke([&] { return graph->NotifyWhenDeviceStarted(inputTrack); })); + + // Output-only. Wait for another second before unmuting. + DispatchFunction([&] { + inputTrack->GraphImpl()->AppendMessage(MakeUnique<GoFaster>(cubeb)); + }); + { + uint32_t totalFrames = 0; + WaitUntil(stream->FramesProcessedEvent(), [&](uint32_t aFrames) { + totalFrames += aFrames; + return totalFrames > static_cast<uint32_t>(graph->GraphRate()); + }); + } + cubeb->DontGoFaster(); + + // Re-open the input to again see that no asserts go off due to bad state. + DispatchFunction([&] { + // Device id does not matter. Ignore. + inputTrack->OpenAudioInput((void*)1, listener); + }); + + stream = WaitFor(cubeb->StreamInitEvent()); + EXPECT_TRUE(stream->mHasInput); + Unused << WaitFor( + Invoke([&] { return graph->NotifyWhenDeviceStarted(inputTrack); })); + + // Full-duplex. Wait for another second before finishing. + DispatchFunction([&] { + inputTrack->GraphImpl()->AppendMessage(MakeUnique<GoFaster>(cubeb)); + }); + { + uint32_t totalFrames = 0; + WaitUntil(stream->FramesProcessedEvent(), [&](uint32_t aFrames) { + totalFrames += aFrames; + return totalFrames > static_cast<uint32_t>(graph->GraphRate()); + }); + } + cubeb->DontGoFaster(); + + // Clean up. + DispatchFunction([&] { + outputTrack->RemoveAudioOutput((void*)1); + outputTrack->Destroy(); + port->Destroy(); + inputTrack->GraphImpl()->AppendMessage( + MakeUnique<StopInputProcessing>(listener)); + Maybe<CubebUtils::AudioDeviceID> id = + Some(reinterpret_cast<CubebUtils::AudioDeviceID>(1)); + inputTrack->CloseAudioInput(id); + inputTrack->Destroy(); + }); + + uint32_t inputRate = stream->InputSampleRate(); + uint32_t inputFrequency = stream->InputFrequency(); + uint64_t preSilenceSamples; + uint32_t estimatedFreq; + uint32_t nrDiscontinuities; + Tie(preSilenceSamples, estimatedFreq, nrDiscontinuities) = + WaitFor(stream->OutputVerificationEvent()); + + EXPECT_EQ(estimatedFreq, inputFrequency); + std::cerr << "PreSilence: " << preSilenceSamples << std::endl; + // We buffer 10ms worth of frames in non-passthrough mode, plus up to 128 + // frames as we round up to the nearest block. See AudioInputProcessing::Pull. + EXPECT_GE(preSilenceSamples, 128U + inputRate / 100); + // If the fallback system clock driver is doing a graph iteration before the + // first audio driver iteration comes in, that iteration is ignored and + // results in zeros. It takes one fallback driver iteration *after* the audio + // driver has started to complete the switch, *usually* resulting two + // 10ms-iterations of silence; sometimes only one. + EXPECT_LE(preSilenceSamples, 128U + 3 * inputRate / 100 /* 3*10ms */); + // The waveform from AudioGenerator starts at 0, but we don't control its + // ending, so we expect a discontinuity there. Note that this check is only + // for the waveform on the stream *after* re-opening the input. + EXPECT_LE(nrDiscontinuities, 1U); +} + +TEST(TestAudioTrackGraph, AudioInputTrackDisabling) +{ + MockCubeb* cubeb = new MockCubeb(); + CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); + + MediaTrackGraph* graph = MediaTrackGraph::GetInstance( + MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*window*/ nullptr, + MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr); + + RefPtr<AudioInputTrack> inputTrack; + RefPtr<ProcessedMediaTrack> outputTrack; + RefPtr<MediaInputPort> port; + RefPtr<AudioInputProcessing> listener; + auto p = Invoke([&] { + inputTrack = AudioInputTrack::Create(graph); + outputTrack = graph->CreateForwardedInputTrack(MediaSegment::AUDIO); + outputTrack->QueueSetAutoend(false); + outputTrack->AddAudioOutput(reinterpret_cast<void*>(1)); + port = outputTrack->AllocateInputPort(inputTrack); + /* Primary graph: Open Audio Input through SourceMediaTrack */ + listener = new AudioInputProcessing(2, PRINCIPAL_HANDLE_NONE); + inputTrack->GraphImpl()->AppendMessage( + MakeUnique<SetPassThrough>(inputTrack, listener, true)); + inputTrack->SetInputProcessing(listener); + inputTrack->OpenAudioInput((void*)1, listener); + inputTrack->GraphImpl()->AppendMessage( + MakeUnique<StartInputProcessing>(inputTrack, listener)); + return graph->NotifyWhenDeviceStarted(inputTrack); + }); + + RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent()); + EXPECT_TRUE(stream->mHasInput); + Unused << WaitFor(p); + + stream->SetOutputRecordingEnabled(true); + + // Wait for a second worth of audio data. GoFaster is dispatched through a + // ControlMessage so that it is called in the first audio driver iteration. + // Otherwise the audio driver might be going very fast while the fallback + // system clock driver is still in an iteration. + DispatchFunction([&] { + inputTrack->GraphImpl()->AppendMessage(MakeUnique<GoFaster>(cubeb)); + }); + uint32_t totalFrames = 0; + WaitUntil(stream->FramesProcessedEvent(), [&](uint32_t aFrames) { + totalFrames += aFrames; + return totalFrames > static_cast<uint32_t>(graph->GraphRate()); + }); + cubeb->DontGoFaster(); + + const uint32_t ITERATION_COUNT = 5; + uint32_t iterations = ITERATION_COUNT; + DisabledTrackMode currentMode = DisabledTrackMode::SILENCE_BLACK; + while (iterations--) { + // toggle the track enabled mode, wait a second, do this ITERATION_COUNT + // times + DispatchFunction([&] { + inputTrack->SetDisabledTrackMode(currentMode); + if (currentMode == DisabledTrackMode::SILENCE_BLACK) { + currentMode = DisabledTrackMode::ENABLED; + } else { + currentMode = DisabledTrackMode::SILENCE_BLACK; + } + inputTrack->GraphImpl()->AppendMessage(MakeUnique<GoFaster>(cubeb)); + }); + + totalFrames = 0; + WaitUntil(stream->FramesProcessedEvent(), [&](uint32_t aFrames) { + totalFrames += aFrames; + return totalFrames > static_cast<uint32_t>(graph->GraphRate()); + }); + cubeb->DontGoFaster(); + } + + // Clean up. + DispatchFunction([&] { + outputTrack->RemoveAudioOutput((void*)1); + outputTrack->Destroy(); + port->Destroy(); + inputTrack->GraphImpl()->AppendMessage( + MakeUnique<StopInputProcessing>(listener)); + Maybe<CubebUtils::AudioDeviceID> id = + Some(reinterpret_cast<CubebUtils::AudioDeviceID>(1)); + inputTrack->CloseAudioInput(id); + inputTrack->Destroy(); + }); + + uint64_t preSilenceSamples; + uint32_t estimatedFreq; + uint32_t nrDiscontinuities; + Tie(preSilenceSamples, estimatedFreq, nrDiscontinuities) = + WaitFor(stream->OutputVerificationEvent()); + + const char* dir = getenv("MOZ_UPLOAD_DIR"); + if (dir && nrDiscontinuities != ITERATION_COUNT) { + WavDumper dumper; + char uploadPath[256]; + SprintfLiteral( + uploadPath, "%s/%s.wav", dir, + ::testing::UnitTest::GetInstance()->current_test_info()->name()); + printf("Writing debug WAV to %s\n", uploadPath); + dumper.OpenExplicit(uploadPath, 1, graph->GraphRate()); + auto data = stream->TakeRecordedOutput(); + dumper.Write(data.Elements(), data.Length()); + } + + // We're enabling/disabling the track ITERATION_COUNT times, so we expect the + // same number of discontinuities. + std::cerr << "nrDiscontinuities" << nrDiscontinuities << std::endl; + EXPECT_EQ(nrDiscontinuities, ITERATION_COUNT); +} + +void TestCrossGraphPort(uint32_t aInputRate, uint32_t aOutputRate, + float aDriftFactor, uint32_t aBufferMs = 50) { + std::cerr << "TestCrossGraphPort input: " << aInputRate + << ", output: " << aOutputRate << ", driftFactor: " << aDriftFactor + << std::endl; + + MockCubeb* cubeb = new MockCubeb(); + CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); + auto unforcer = WaitFor(cubeb->ForceAudioThread()).unwrap(); + Unused << unforcer; + + cubeb->SetStreamStartFreezeEnabled(true); + + /* Primary graph: Create the graph. */ + MediaTrackGraph* primary = + MediaTrackGraph::GetInstance(MediaTrackGraph::SYSTEM_THREAD_DRIVER, + /*window*/ nullptr, aInputRate, nullptr); + + /* Partner graph: Create the graph. */ + MediaTrackGraph* partner = MediaTrackGraph::GetInstance( + MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*window*/ nullptr, aOutputRate, + /*OutputDeviceID*/ reinterpret_cast<cubeb_devid>(1)); + + RefPtr<AudioInputTrack> inputTrack; + RefPtr<AudioInputProcessing> listener; + auto primaryStarted = Invoke([&] { + /* Primary graph: Create input track and open it */ + inputTrack = AudioInputTrack::Create(primary); + listener = new AudioInputProcessing(2, PRINCIPAL_HANDLE_NONE); + inputTrack->GraphImpl()->AppendMessage( + MakeUnique<SetPassThrough>(inputTrack, listener, true)); + inputTrack->SetInputProcessing(listener); + inputTrack->GraphImpl()->AppendMessage( + MakeUnique<StartInputProcessing>(inputTrack, listener)); + inputTrack->OpenAudioInput((void*)1, listener); + return primary->NotifyWhenDeviceStarted(inputTrack); + }); + + RefPtr<SmartMockCubebStream> inputStream = WaitFor(cubeb->StreamInitEvent()); + + RefPtr<CrossGraphTransmitter> transmitter; + RefPtr<MediaInputPort> port; + RefPtr<CrossGraphReceiver> receiver; + auto partnerStarted = Invoke([&] { + /* Partner graph: Create CrossGraphReceiver */ + receiver = partner->CreateCrossGraphReceiver(primary->GraphRate()); + + /* Primary graph: Create CrossGraphTransmitter */ + transmitter = primary->CreateCrossGraphTransmitter(receiver); + + /* How the input track connects to another ProcessedMediaTrack. + * Check in MediaManager how it is connected to AudioStreamTrack. */ + port = transmitter->AllocateInputPort(inputTrack); + receiver->AddAudioOutput((void*)1); + return partner->NotifyWhenDeviceStarted(receiver); + }); + + RefPtr<SmartMockCubebStream> partnerStream = + WaitFor(cubeb->StreamInitEvent()); + partnerStream->SetDriftFactor(aDriftFactor); + + cubeb->SetStreamStartFreezeEnabled(false); + + // One source of non-determinism in this type of test is that inputStream + // and partnerStream are started in sequence by the CubebOperation thread pool + // (of size 1). To minimize the chance that the stream that starts first sees + // an iteration before the other has started - this is a source of pre-silence + // - we freeze both on start and thaw them together here. + // Note that another source of non-determinism is the fallback driver. Handing + // over from the fallback to the audio driver requires first an audio callback + // (deterministic with the fake audio thread), then a fallback driver + // iteration (non-deterministic, since each graph has its own fallback driver, + // each with its own dedicated thread, which we have no control over). This + // non-determinism is worrisome, but both fallback drivers are likely to + // exhibit similar characteristics, hopefully keeping the level of + // non-determinism down sufficiently for this test to pass. + inputStream->Thaw(); + partnerStream->Thaw(); + + Unused << WaitFor(primaryStarted); + Unused << WaitFor(partnerStarted); + + // Wait for 3s worth of audio data on the receiver stream. + DispatchFunction([&] { + inputTrack->GraphImpl()->AppendMessage(MakeUnique<GoFaster>(cubeb)); + }); + uint32_t totalFrames = 0; + WaitUntil(partnerStream->FramesVerifiedEvent(), [&](uint32_t aFrames) { + totalFrames += aFrames; + return totalFrames > static_cast<uint32_t>(partner->GraphRate() * 3); + }); + cubeb->DontGoFaster(); + + DispatchFunction([&] { + // Clean up on MainThread + receiver->RemoveAudioOutput((void*)1); + receiver->Destroy(); + transmitter->Destroy(); + port->Destroy(); + inputTrack->GraphImpl()->AppendMessage( + MakeUnique<StopInputProcessing>(listener)); + Maybe<CubebUtils::AudioDeviceID> id = + Some(reinterpret_cast<CubebUtils::AudioDeviceID>(1)); + inputTrack->CloseAudioInput(id); + inputTrack->Destroy(); + }); + + uint32_t inputFrequency = inputStream->InputFrequency(); + uint32_t partnerRate = partnerStream->InputSampleRate(); + + uint64_t preSilenceSamples; + float estimatedFreq; + uint32_t nrDiscontinuities; + Tie(preSilenceSamples, estimatedFreq, nrDiscontinuities) = + WaitFor(partnerStream->OutputVerificationEvent()); + + EXPECT_NEAR(estimatedFreq, inputFrequency / aDriftFactor, 5); + uint32_t expectedPreSilence = + static_cast<uint32_t>(partnerRate * aDriftFactor / 1000 * aBufferMs); + uint32_t margin = partnerRate / 20 /* +/- 50ms */; + EXPECT_NEAR(preSilenceSamples, expectedPreSilence, margin); + // The waveform from AudioGenerator starts at 0, but we don't control its + // ending, so we expect a discontinuity there. + EXPECT_LE(nrDiscontinuities, 1U); +} + +TEST(TestAudioTrackGraph, CrossGraphPort) +{ + TestCrossGraphPort(44100, 44100, 1); + TestCrossGraphPort(44100, 44100, 1.08); + TestCrossGraphPort(44100, 44100, 0.92); + + TestCrossGraphPort(48000, 44100, 1); + TestCrossGraphPort(48000, 44100, 1.08); + TestCrossGraphPort(48000, 44100, 0.92); + + TestCrossGraphPort(44100, 48000, 1); + TestCrossGraphPort(44100, 48000, 1.08); + TestCrossGraphPort(44100, 48000, 0.92); + + TestCrossGraphPort(52110, 17781, 1); + TestCrossGraphPort(52110, 17781, 1.08); + TestCrossGraphPort(52110, 17781, 0.92); +} + +TEST(TestAudioTrackGraph, CrossGraphPortLargeBuffer) +{ + const int32_t oldBuffering = Preferences::GetInt(DRIFT_BUFFERING_PREF); + const int32_t longBuffering = 5000; + Preferences::SetInt(DRIFT_BUFFERING_PREF, longBuffering); + + TestCrossGraphPort(44100, 44100, 1.02, longBuffering); + TestCrossGraphPort(48000, 44100, 1.08, longBuffering); + TestCrossGraphPort(44100, 48000, 0.95, longBuffering); + TestCrossGraphPort(52110, 17781, 0.92, longBuffering); + + Preferences::SetInt(DRIFT_BUFFERING_PREF, oldBuffering); +} +#endif // MOZ_WEBRTC diff --git a/dom/media/gtest/TestBenchmarkStorage.cpp b/dom/media/gtest/TestBenchmarkStorage.cpp new file mode 100644 index 0000000000..0f1eb7e4c4 --- /dev/null +++ b/dom/media/gtest/TestBenchmarkStorage.cpp @@ -0,0 +1,92 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/BenchmarkStorageParent.h" + +#include "gmock/gmock.h" +#include "gtest/gtest-printers.h" +#include "gtest/gtest.h" + +using ::testing::Return; +using namespace mozilla; + +TEST(BenchmarkStorage, MovingAverage) +{ + int32_t av = 0; + int32_t win = 0; + int32_t val = 100; + BenchmarkStorageParent::MovingAverage(av, win, val); + EXPECT_EQ(av, val) << "1st average"; + EXPECT_EQ(win, 1) << "1st window"; + + av = 50; + win = 1; + val = 100; + BenchmarkStorageParent::MovingAverage(av, win, val); + EXPECT_EQ(av, 75) << "2nd average"; + EXPECT_EQ(win, 2) << "2nd window"; + + av = 100; + win = 9; + val = 90; + BenchmarkStorageParent::MovingAverage(av, win, val); + EXPECT_EQ(av, 99) << "9th average"; + EXPECT_EQ(win, 10) << "9th window"; + + av = 90; + win = 19; + val = 90; + BenchmarkStorageParent::MovingAverage(av, win, val); + EXPECT_EQ(av, 90) << "19th average"; + EXPECT_EQ(win, 20) << "19th window"; + + av = 90; + win = 20; + val = 100; + BenchmarkStorageParent::MovingAverage(av, win, val); + EXPECT_EQ(av, 91) << "20th average"; + EXPECT_EQ(win, 20) << "20th window"; +} + +TEST(BenchmarkStorage, ParseStoredValue) +{ + int32_t win = 0; + int32_t score = BenchmarkStorageParent::ParseStoredValue(1100, win); + EXPECT_EQ(win, 1) << "Window"; + EXPECT_EQ(score, 100) << "Score/Percentage"; + + win = 0; + score = BenchmarkStorageParent::ParseStoredValue(10099, win); + EXPECT_EQ(win, 10) << "Window"; + EXPECT_EQ(score, 99) << "Score/Percentage"; + + win = 0; + score = BenchmarkStorageParent::ParseStoredValue(15038, win); + EXPECT_EQ(win, 15) << "Window"; + EXPECT_EQ(score, 38) << "Score/Percentage"; + + win = 0; + score = BenchmarkStorageParent::ParseStoredValue(20099, win); + EXPECT_EQ(win, 20) << "Window"; + EXPECT_EQ(score, 99) << "Score/Percentage"; +} + +TEST(BenchmarkStorage, PrepareStoredValue) +{ + int32_t stored_value = BenchmarkStorageParent::PrepareStoredValue(80, 1); + EXPECT_EQ(stored_value, 1080) << "Window"; + + stored_value = BenchmarkStorageParent::PrepareStoredValue(100, 6); + EXPECT_EQ(stored_value, 6100) << "Window"; + + stored_value = BenchmarkStorageParent::PrepareStoredValue(1, 10); + EXPECT_EQ(stored_value, 10001) << "Window"; + + stored_value = BenchmarkStorageParent::PrepareStoredValue(88, 13); + EXPECT_EQ(stored_value, 13088) << "Window"; + + stored_value = BenchmarkStorageParent::PrepareStoredValue(100, 20); + EXPECT_EQ(stored_value, 20100) << "Window"; +} diff --git a/dom/media/gtest/TestBitWriter.cpp b/dom/media/gtest/TestBitWriter.cpp new file mode 100644 index 0000000000..1464a11f63 --- /dev/null +++ b/dom/media/gtest/TestBitWriter.cpp @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "BitReader.h" +#include "BitWriter.h" +#include "H264.h" + +using namespace mozilla; + +TEST(BitWriter, BitWriter) +{ + RefPtr<MediaByteBuffer> test = new MediaByteBuffer(); + BitWriter b(test); + b.WriteBit(false); + b.WriteBits(~1ULL, 1); // ensure that extra bits don't modify byte buffer. + b.WriteBits(3, 1); + b.WriteUE(1280 / 16 - 1); + b.WriteUE(720 / 16 - 1); + b.WriteUE(1280); + b.WriteUE(720); + b.WriteBit(true); + b.WriteBit(false); + b.WriteBit(true); + b.WriteU8(7); + b.WriteU32(16356); + b.WriteU64(116356); + b.WriteBits(~(0ULL) & ~1ULL, 16); + const uint32_t length = b.BitCount(); + b.CloseWithRbspTrailing(); + + BitReader c(test); + + EXPECT_EQ(c.ReadBit(), false); + EXPECT_EQ(c.ReadBit(), false); + EXPECT_EQ(c.ReadBit(), true); + EXPECT_EQ(c.ReadUE(), 1280u / 16 - 1); + EXPECT_EQ(c.ReadUE(), 720u / 16 - 1); + EXPECT_EQ(c.ReadUE(), 1280u); + EXPECT_EQ(c.ReadUE(), 720u); + EXPECT_EQ(c.ReadBit(), true); + EXPECT_EQ(c.ReadBit(), false); + EXPECT_EQ(c.ReadBit(), true); + EXPECT_EQ(c.ReadBits(8), 7u); + EXPECT_EQ(c.ReadU32(), 16356u); + EXPECT_EQ(c.ReadU64(), 116356u); + EXPECT_EQ(c.ReadBits(16), 0xfffeu); + EXPECT_EQ(length, BitReader::GetBitLength(test)); +} + +TEST(BitWriter, SPS) +{ + uint8_t sps_pps[] = {0x01, 0x4d, 0x40, 0x0c, 0xff, 0xe1, 0x00, 0x1b, 0x67, + 0x4d, 0x40, 0x0c, 0xe8, 0x80, 0x80, 0x9d, 0x80, 0xb5, + 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x40, + 0x00, 0x00, 0x0f, 0x03, 0xc5, 0x0a, 0x44, 0x80, 0x01, + 0x00, 0x04, 0x68, 0xeb, 0xef, 0x20}; + + RefPtr<MediaByteBuffer> extraData = new MediaByteBuffer(); + extraData->AppendElements(sps_pps, sizeof(sps_pps)); + SPSData spsdata1; + bool success = H264::DecodeSPSFromExtraData(extraData, spsdata1); + EXPECT_EQ(success, true); + + RefPtr<MediaByteBuffer> extraData2 = + H264::CreateExtraData(0x42, 0xc0, 0x1e, {1280, 720}); + SPSData spsdata2; + success = H264::DecodeSPSFromExtraData(extraData2, spsdata2); + EXPECT_EQ(success, true); +} diff --git a/dom/media/gtest/TestBlankVideoDataCreator.cpp b/dom/media/gtest/TestBlankVideoDataCreator.cpp new file mode 100644 index 0000000000..b30f1cecbe --- /dev/null +++ b/dom/media/gtest/TestBlankVideoDataCreator.cpp @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "BlankDecoderModule.h" +#include "ImageContainer.h" + +using namespace mozilla; + +TEST(BlankVideoDataCreator, ShouldNotOverflow) +{ + RefPtr<MediaRawData> mrd = new MediaRawData(); + const uint32_t width = 1; + const uint32_t height = 1; + BlankVideoDataCreator creater(width, height, nullptr); + RefPtr<MediaData> data = creater.Create(mrd); + EXPECT_NE(data.get(), nullptr); +} + +TEST(BlankVideoDataCreator, ShouldOverflow) +{ + RefPtr<MediaRawData> mrd = new MediaRawData(); + const uint32_t width = UINT_MAX; + const uint32_t height = UINT_MAX; + BlankVideoDataCreator creater(width, height, nullptr); + RefPtr<MediaData> data = creater.Create(mrd); + EXPECT_EQ(data.get(), nullptr); +} diff --git a/dom/media/gtest/TestBufferReader.cpp b/dom/media/gtest/TestBufferReader.cpp new file mode 100644 index 0000000000..63ffc242e4 --- /dev/null +++ b/dom/media/gtest/TestBufferReader.cpp @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "BufferReader.h" + +using namespace mozilla; + +TEST(BufferReader, ReaderCursor) +{ + // Allocate a buffer and create a BufferReader. + const size_t BUFFER_SIZE = 10; + uint8_t buffer[BUFFER_SIZE] = {0}; + + const uint8_t* const HEAD = reinterpret_cast<uint8_t*>(buffer); + const uint8_t* const TAIL = HEAD + BUFFER_SIZE; + + BufferReader reader(HEAD, BUFFER_SIZE); + ASSERT_EQ(reader.Offset(), static_cast<size_t>(0)); + ASSERT_EQ(reader.Peek(BUFFER_SIZE), HEAD); + + // Keep reading to the end, and make sure the final read failed. + const size_t READ_SIZE = 4; + ASSERT_NE(BUFFER_SIZE % READ_SIZE, static_cast<size_t>(0)); + for (const uint8_t* ptr = reader.Peek(0); ptr != nullptr; + ptr = reader.Read(READ_SIZE)) { + } + + // Check the reading cursor of the BufferReader is correct + // after reading and seeking. + const uint8_t* tail = reader.Peek(0); + const uint8_t* head = reader.Seek(0); + + EXPECT_EQ(head, HEAD); + EXPECT_EQ(tail, TAIL); +} diff --git a/dom/media/gtest/TestCDMStorage.cpp b/dom/media/gtest/TestCDMStorage.cpp new file mode 100644 index 0000000000..47ef67105a --- /dev/null +++ b/dom/media/gtest/TestCDMStorage.cpp @@ -0,0 +1,1074 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ChromiumCDMCallback.h" +#include "ChromiumCDMParent.h" +#include "GMPServiceParent.h" +#include "GMPTestMonitor.h" +#include "MediaResult.h" +#include "gtest/gtest.h" +#include "mozilla/RefPtr.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "nsIFile.h" +#include "nsNSSComponent.h" //For EnsureNSSInitializedChromeOrContent +#include "nsThreadUtils.h" + +using namespace mozilla; +using namespace mozilla::gmp; + +static already_AddRefed<nsIThread> GetGMPThread() { + RefPtr<GeckoMediaPluginService> service = + GeckoMediaPluginService::GetGeckoMediaPluginService(); + nsCOMPtr<nsIThread> thread; + EXPECT_TRUE(NS_SUCCEEDED(service->GetThread(getter_AddRefs(thread)))); + return thread.forget(); +} + +/** + * Enumerate files under |aPath| (non-recursive). + */ +template <typename T> +static nsresult EnumerateDir(nsIFile* aPath, T&& aDirIter) { + nsCOMPtr<nsIDirectoryEnumerator> iter; + nsresult rv = aPath->GetDirectoryEntries(getter_AddRefs(iter)); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIFile> entry; + while (NS_SUCCEEDED(iter->GetNextFile(getter_AddRefs(entry))) && entry) { + aDirIter(entry); + } + return NS_OK; +} + +/** + * Enumerate files under $profileDir/gmp/$platform/gmp-fake/$aDir/ + * (non-recursive). + */ +template <typename T> +static nsresult EnumerateCDMStorageDir(const nsACString& aDir, T&& aDirIter) { + RefPtr<GeckoMediaPluginServiceParent> service = + GeckoMediaPluginServiceParent::GetSingleton(); + MOZ_ASSERT(service); + + // $profileDir/gmp/$platform/ + nsCOMPtr<nsIFile> path; + nsresult rv = service->GetStorageDir(getter_AddRefs(path)); + if (NS_FAILED(rv)) { + return rv; + } + + // $profileDir/gmp/$platform/gmp-fake/ + rv = path->Append(u"gmp-fake"_ns); + if (NS_FAILED(rv)) { + return rv; + } + + // $profileDir/gmp/$platform/gmp-fake/$aDir/ + rv = path->AppendNative(aDir); + if (NS_FAILED(rv)) { + return rv; + } + + return EnumerateDir(path, aDirIter); +} + +class GMPShutdownObserver : public nsIRunnable, public nsIObserver { + public: + GMPShutdownObserver(already_AddRefed<nsIRunnable> aShutdownTask, + already_AddRefed<nsIRunnable> Continuation, + const nsACString& aNodeId) + : mShutdownTask(aShutdownTask), + mContinuation(Continuation), + mNodeId(NS_ConvertUTF8toUTF16(aNodeId)) {} + + NS_DECL_THREADSAFE_ISUPPORTS + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + EXPECT_TRUE(observerService); + observerService->AddObserver(this, "gmp-shutdown", false); + + nsCOMPtr<nsIThread> thread(GetGMPThread()); + thread->Dispatch(mShutdownTask, NS_DISPATCH_NORMAL); + return NS_OK; + } + + NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aSomeData) override { + if (!strcmp(aTopic, "gmp-shutdown") && + mNodeId.Equals(nsDependentString(aSomeData))) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + EXPECT_TRUE(observerService); + observerService->RemoveObserver(this, "gmp-shutdown"); + nsCOMPtr<nsIThread> thread(GetGMPThread()); + thread->Dispatch(mContinuation, NS_DISPATCH_NORMAL); + } + return NS_OK; + } + + private: + virtual ~GMPShutdownObserver() = default; + nsCOMPtr<nsIRunnable> mShutdownTask; + nsCOMPtr<nsIRunnable> mContinuation; + const nsString mNodeId; +}; + +NS_IMPL_ISUPPORTS(GMPShutdownObserver, nsIRunnable, nsIObserver) + +class NotifyObserversTask : public Runnable { + public: + explicit NotifyObserversTask(const char* aTopic) + : mozilla::Runnable("NotifyObserversTask"), mTopic(aTopic) {} + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->NotifyObservers(nullptr, mTopic, nullptr); + } + return NS_OK; + } + const char* mTopic; +}; + +class ClearCDMStorageTask : public nsIRunnable, public nsIObserver { + public: + ClearCDMStorageTask(already_AddRefed<nsIRunnable> Continuation, + nsIThread* aTarget, PRTime aSince) + : mContinuation(Continuation), mTarget(aTarget), mSince(aSince) {} + + NS_DECL_THREADSAFE_ISUPPORTS + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + EXPECT_TRUE(observerService); + observerService->AddObserver(this, "gmp-clear-storage-complete", false); + if (observerService) { + nsAutoString str; + if (mSince >= 0) { + str.AppendInt(static_cast<int64_t>(mSince)); + } + observerService->NotifyObservers(nullptr, "browser:purge-session-history", + str.Data()); + } + return NS_OK; + } + + NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aSomeData) override { + if (!strcmp(aTopic, "gmp-clear-storage-complete")) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + EXPECT_TRUE(observerService); + observerService->RemoveObserver(this, "gmp-clear-storage-complete"); + mTarget->Dispatch(mContinuation, NS_DISPATCH_NORMAL); + } + return NS_OK; + } + + private: + virtual ~ClearCDMStorageTask() = default; + nsCOMPtr<nsIRunnable> mContinuation; + nsCOMPtr<nsIThread> mTarget; + const PRTime mSince; +}; + +NS_IMPL_ISUPPORTS(ClearCDMStorageTask, nsIRunnable, nsIObserver) + +static void ClearCDMStorage(already_AddRefed<nsIRunnable> aContinuation, + nsIThread* aTarget, PRTime aSince = -1) { + RefPtr<ClearCDMStorageTask> task( + new ClearCDMStorageTask(std::move(aContinuation), aTarget, aSince)); + SchedulerGroup::Dispatch(TaskCategory::Other, task.forget()); +} + +static void SimulatePBModeExit() { + NS_DispatchToMainThread(new NotifyObserversTask("last-pb-context-exited"), + NS_DISPATCH_SYNC); +} + +class TestGetNodeIdCallback : public GetNodeIdCallback { + public: + TestGetNodeIdCallback(nsCString& aNodeId, nsresult& aResult) + : mNodeId(aNodeId), mResult(aResult) {} + + void Done(nsresult aResult, const nsACString& aNodeId) { + mResult = aResult; + mNodeId = aNodeId; + } + + private: + nsCString& mNodeId; + nsresult& mResult; +}; + +static NodeIdParts GetNodeIdParts(const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, + const nsAString& aGmpName, bool aInPBMode) { + OriginAttributes attrs; + attrs.mPrivateBrowsingId = aInPBMode ? 1 : 0; + + nsAutoCString suffix; + attrs.CreateSuffix(suffix); + + nsAutoString origin; + origin.Assign(aOrigin); + origin.Append(NS_ConvertUTF8toUTF16(suffix)); + + nsAutoString topLevelOrigin; + topLevelOrigin.Assign(aTopLevelOrigin); + topLevelOrigin.Append(NS_ConvertUTF8toUTF16(suffix)); + return NodeIdParts{origin, topLevelOrigin, nsString(aGmpName)}; +} + +static nsCString GetNodeId(const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, bool aInPBMode) { + RefPtr<GeckoMediaPluginServiceParent> service = + GeckoMediaPluginServiceParent::GetSingleton(); + EXPECT_TRUE(service); + nsCString nodeId; + nsresult result; + UniquePtr<GetNodeIdCallback> callback( + new TestGetNodeIdCallback(nodeId, result)); + + OriginAttributes attrs; + attrs.mPrivateBrowsingId = aInPBMode ? 1 : 0; + + nsAutoCString suffix; + attrs.CreateSuffix(suffix); + + nsAutoString origin; + origin.Assign(aOrigin); + origin.Append(NS_ConvertUTF8toUTF16(suffix)); + + nsAutoString topLevelOrigin; + topLevelOrigin.Assign(aTopLevelOrigin); + topLevelOrigin.Append(NS_ConvertUTF8toUTF16(suffix)); + + // We rely on the fact that the GetNodeId implementation for + // GeckoMediaPluginServiceParent is synchronous. + nsresult rv = service->GetNodeId(origin, topLevelOrigin, u"gmp-fake"_ns, + std::move(callback)); + EXPECT_TRUE(NS_SUCCEEDED(rv) && NS_SUCCEEDED(result)); + return nodeId; +} + +static bool IsCDMStorageIsEmpty() { + RefPtr<GeckoMediaPluginServiceParent> service = + GeckoMediaPluginServiceParent::GetSingleton(); + MOZ_ASSERT(service); + nsCOMPtr<nsIFile> storage; + nsresult rv = service->GetStorageDir(getter_AddRefs(storage)); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + bool exists = false; + if (storage) { + storage->Exists(&exists); + } + return !exists; +} + +static void AssertIsOnGMPThread() { + RefPtr<GeckoMediaPluginService> service = + GeckoMediaPluginService::GetGeckoMediaPluginService(); + MOZ_ASSERT(service); + nsCOMPtr<nsIThread> thread; + service->GetThread(getter_AddRefs(thread)); + MOZ_ASSERT(thread); + nsCOMPtr<nsIThread> currentThread; + DebugOnly<nsresult> rv = NS_GetCurrentThread(getter_AddRefs(currentThread)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + MOZ_ASSERT(currentThread == thread); +} + +class CDMStorageTest { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CDMStorageTest) + + void DoTest(void (CDMStorageTest::*aTestMethod)()) { + EnsureNSSInitializedChromeOrContent(); + nsCOMPtr<nsIThread> thread(GetGMPThread()); + ClearCDMStorage( + NewRunnableMethod("CDMStorageTest::DoTest", this, aTestMethod), thread); + AwaitFinished(); + } + + CDMStorageTest() : mMonitor("CDMStorageTest"), mFinished(false) {} + + void Update(const nsCString& aMessage) { + nsTArray<uint8_t> msg; + msg.AppendElements(aMessage.get(), aMessage.Length()); + mCDM->UpdateSession("fake-session-id"_ns, 1, msg); + } + + void TestGetNodeId() { + AssertIsOnGMPThread(); + + EXPECT_TRUE(IsCDMStorageIsEmpty()); + + const nsString origin1 = u"http://example1.com"_ns; + const nsString origin2 = u"http://example2.org"_ns; + + nsCString PBnodeId1 = GetNodeId(origin1, origin2, true); + nsCString PBnodeId2 = GetNodeId(origin1, origin2, true); + + // Node ids for the same origins should be the same in PB mode. + EXPECT_TRUE(PBnodeId1.Equals(PBnodeId2)); + + nsCString PBnodeId3 = GetNodeId(origin2, origin1, true); + + // Node ids with origin and top level origin swapped should be different. + EXPECT_TRUE(!PBnodeId3.Equals(PBnodeId1)); + + // Getting node ids in PB mode should not result in the node id being + // stored. + EXPECT_TRUE(IsCDMStorageIsEmpty()); + + nsCString nodeId1 = GetNodeId(origin1, origin2, false); + nsCString nodeId2 = GetNodeId(origin1, origin2, false); + + // NodeIds for the same origin pair in non-pb mode should be the same. + EXPECT_TRUE(nodeId1.Equals(nodeId2)); + + // Node ids for a given origin pair should be different for the PB origins + // should be the same in PB mode. + EXPECT_TRUE(!PBnodeId1.Equals(nodeId1)); + EXPECT_TRUE(!PBnodeId2.Equals(nodeId2)); + + nsCOMPtr<nsIThread> thread(GetGMPThread()); + ClearCDMStorage(NewRunnableMethod<nsCString>( + "CDMStorageTest::TestGetNodeId_Continuation", this, + &CDMStorageTest::TestGetNodeId_Continuation, nodeId1), + thread); + } + + void TestGetNodeId_Continuation(nsCString aNodeId1) { + EXPECT_TRUE(IsCDMStorageIsEmpty()); + + // Once we clear storage, the node ids generated for the same origin-pair + // should be different. + const nsString origin1 = u"http://example1.com"_ns; + const nsString origin2 = u"http://example2.org"_ns; + nsCString nodeId3 = GetNodeId(origin1, origin2, false); + EXPECT_TRUE(!aNodeId1.Equals(nodeId3)); + + SetFinished(); + } + + void CreateDecryptor(const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, bool aInPBMode, + const nsCString& aUpdate) { + nsTArray<nsCString> updates; + updates.AppendElement(aUpdate); + CreateDecryptor(aOrigin, aTopLevelOrigin, aInPBMode, std::move(updates)); + } + + void CreateDecryptor(const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, bool aInPBMode, + nsTArray<nsCString>&& aUpdates) { + CreateDecryptor( + GetNodeIdParts(aOrigin, aTopLevelOrigin, u"gmp-fake"_ns, aInPBMode), + std::move(aUpdates)); + } + + void CreateDecryptor(const NodeIdParts& aNodeId, + nsTArray<nsCString>&& aUpdates) { + RefPtr<GeckoMediaPluginService> service = + GeckoMediaPluginService::GetGeckoMediaPluginService(); + EXPECT_TRUE(service); + + nsTArray<nsCString> tags; + tags.AppendElement("fake"_ns); + + RefPtr<CDMStorageTest> self = this; + RefPtr<gmp::GetCDMParentPromise> promise = + service->GetCDM(aNodeId, std::move(tags), nullptr); + nsCOMPtr<nsISerialEventTarget> thread = GetGMPThread(); + promise->Then( + thread, __func__, + [self, updates = std::move(aUpdates), + thread](RefPtr<gmp::ChromiumCDMParent> cdm) mutable { + self->mCDM = cdm; + EXPECT_TRUE(!!self->mCDM); + self->mCallback.reset(new CallbackProxy(self)); + nsCString failureReason; + self->mCDM + ->Init(self->mCallback.get(), false, true, + GetMainThreadEventTarget()) + ->Then( + thread, __func__, + [self, updates = std::move(updates)] { + for (const auto& update : updates) { + self->Update(update); + } + }, + [](MediaResult rv) { EXPECT_TRUE(false); }); + }, + [](MediaResult rv) { EXPECT_TRUE(false); }); + } + + void TestBasicStorage() { + AssertIsOnGMPThread(); + EXPECT_TRUE(IsCDMStorageIsEmpty()); + + RefPtr<GeckoMediaPluginService> service = + GeckoMediaPluginService::GetGeckoMediaPluginService(); + + // Send a message to the fake GMP for it to run its own tests internally. + // It sends us a "test-storage complete" message when its passed, or + // some other message if its tests fail. + Expect("test-storage complete"_ns, + NewRunnableMethod("CDMStorageTest::SetFinished", this, + &CDMStorageTest::SetFinished)); + + CreateDecryptor(u"http://example1.com"_ns, u"http://example2.com"_ns, false, + "test-storage"_ns); + } + + /** + * 1. Generate storage data for some sites. + * 2. Forget about one of the sites. + * 3. Check if the storage data for the forgotten site are erased correctly. + * 4. Check if the storage data for other sites remain unchanged. + */ + void TestForgetThisSite() { + AssertIsOnGMPThread(); + EXPECT_TRUE(IsCDMStorageIsEmpty()); + + // Generate storage data for some site. + nsCOMPtr<nsIRunnable> r = NewRunnableMethod( + "CDMStorageTest::TestForgetThisSite_AnotherSite", this, + &CDMStorageTest::TestForgetThisSite_AnotherSite); + Expect("test-storage complete"_ns, r.forget()); + + CreateDecryptor(u"http://example1.com"_ns, u"http://example2.com"_ns, false, + "test-storage"_ns); + } + + void TestForgetThisSite_AnotherSite() { + Shutdown(); + + // Generate storage data for another site. + nsCOMPtr<nsIRunnable> r = NewRunnableMethod( + "CDMStorageTest::TestForgetThisSite_CollectSiteInfo", this, + &CDMStorageTest::TestForgetThisSite_CollectSiteInfo); + Expect("test-storage complete"_ns, r.forget()); + + CreateDecryptor(u"http://example3.com"_ns, u"http://example4.com"_ns, false, + "test-storage"_ns); + } + + struct NodeInfo { + explicit NodeInfo(const nsACString& aSite, + const mozilla::OriginAttributesPattern& aPattern) + : siteToForget(aSite), mPattern(aPattern) {} + nsCString siteToForget; + mozilla::OriginAttributesPattern mPattern; + nsTArray<nsCString> expectedRemainingNodeIds; + }; + + class NodeIdCollector { + public: + explicit NodeIdCollector(NodeInfo* aInfo) : mNodeInfo(aInfo) {} + void operator()(nsIFile* aFile) { + nsCString salt; + nsresult rv = ReadSalt(aFile, salt); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + if (!MatchOrigin(aFile, mNodeInfo->siteToForget, mNodeInfo->mPattern)) { + mNodeInfo->expectedRemainingNodeIds.AppendElement(salt); + } + } + + private: + NodeInfo* mNodeInfo; + }; + + void TestForgetThisSite_CollectSiteInfo() { + mozilla::OriginAttributesPattern pattern; + + UniquePtr<NodeInfo> siteInfo( + new NodeInfo("http://example1.com"_ns, pattern)); + // Collect nodeIds that are expected to remain for later comparison. + EnumerateCDMStorageDir("id"_ns, NodeIdCollector(siteInfo.get())); + // Invoke "Forget this site" on the main thread. + SchedulerGroup::Dispatch( + TaskCategory::Other, + NewRunnableMethod<UniquePtr<NodeInfo>&&>( + "CDMStorageTest::TestForgetThisSite_Forget", this, + &CDMStorageTest::TestForgetThisSite_Forget, std::move(siteInfo))); + } + + void TestForgetThisSite_Forget(UniquePtr<NodeInfo>&& aSiteInfo) { + RefPtr<GeckoMediaPluginServiceParent> service = + GeckoMediaPluginServiceParent::GetSingleton(); + service->ForgetThisSiteNative( + NS_ConvertUTF8toUTF16(aSiteInfo->siteToForget), aSiteInfo->mPattern); + + nsCOMPtr<nsIThread> thread; + service->GetThread(getter_AddRefs(thread)); + + nsCOMPtr<nsIRunnable> r = NewRunnableMethod<UniquePtr<NodeInfo>&&>( + "CDMStorageTest::TestForgetThisSite_Verify", this, + &CDMStorageTest::TestForgetThisSite_Verify, std::move(aSiteInfo)); + thread->Dispatch(r, NS_DISPATCH_NORMAL); + + nsCOMPtr<nsIRunnable> f = NewRunnableMethod( + "CDMStorageTest::SetFinished", this, &CDMStorageTest::SetFinished); + thread->Dispatch(f, NS_DISPATCH_NORMAL); + } + + class NodeIdVerifier { + public: + explicit NodeIdVerifier(const NodeInfo* aInfo) + : mNodeInfo(aInfo), + mExpectedRemainingNodeIds(aInfo->expectedRemainingNodeIds.Clone()) {} + void operator()(nsIFile* aFile) { + nsCString salt; + nsresult rv = ReadSalt(aFile, salt); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + // Shouldn't match the origin if we clear correctly. + EXPECT_FALSE( + MatchOrigin(aFile, mNodeInfo->siteToForget, mNodeInfo->mPattern)); + // Check if remaining nodeIDs are as expected. + EXPECT_TRUE(mExpectedRemainingNodeIds.RemoveElement(salt)); + } + ~NodeIdVerifier() { EXPECT_TRUE(mExpectedRemainingNodeIds.IsEmpty()); } + + private: + const NodeInfo* mNodeInfo; + nsTArray<nsCString> mExpectedRemainingNodeIds; + }; + + class StorageVerifier { + public: + explicit StorageVerifier(const NodeInfo* aInfo) + : mExpectedRemainingNodeIds(aInfo->expectedRemainingNodeIds.Clone()) {} + void operator()(nsIFile* aFile) { + nsCString salt; + nsresult rv = aFile->GetNativeLeafName(salt); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + EXPECT_TRUE(mExpectedRemainingNodeIds.RemoveElement(salt)); + } + ~StorageVerifier() { EXPECT_TRUE(mExpectedRemainingNodeIds.IsEmpty()); } + + private: + nsTArray<nsCString> mExpectedRemainingNodeIds; + }; + + void TestForgetThisSite_Verify(UniquePtr<NodeInfo>&& aSiteInfo) { + nsresult rv = + EnumerateCDMStorageDir("id"_ns, NodeIdVerifier(aSiteInfo.get())); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + + rv = EnumerateCDMStorageDir("storage"_ns, StorageVerifier(aSiteInfo.get())); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + } + + /** + * 1. Generate some storage data. + * 2. Find the max mtime |t| in $profileDir/gmp/$platform/gmp-fake/id/. + * 3. Pass |t| to clear recent history. + * 4. Check if all directories in $profileDir/gmp/$platform/gmp-fake/id/ and + * $profileDir/gmp/$platform/gmp-fake/storage are removed. + */ + void TestClearRecentHistory1() { + AssertIsOnGMPThread(); + EXPECT_TRUE(IsCDMStorageIsEmpty()); + + // Generate storage data for some site. + nsCOMPtr<nsIRunnable> r = + NewRunnableMethod("CDMStorageTest::TestClearRecentHistory1_Clear", this, + &CDMStorageTest::TestClearRecentHistory1_Clear); + Expect("test-storage complete"_ns, r.forget()); + + CreateDecryptor(u"http://example1.com"_ns, u"http://example2.com"_ns, false, + "test-storage"_ns); + } + + /** + * 1. Generate some storage data. + * 2. Find the max mtime |t| in $profileDir/gmp/$platform/gmp-fake/storage/. + * 3. Pass |t| to clear recent history. + * 4. Check if all directories in $profileDir/gmp/$platform/gmp-fake/id/ and + * $profileDir/gmp/$platform/gmp-fake/storage are removed. + */ + void TestClearRecentHistory2() { + AssertIsOnGMPThread(); + EXPECT_TRUE(IsCDMStorageIsEmpty()); + + // Generate storage data for some site. + nsCOMPtr<nsIRunnable> r = + NewRunnableMethod("CDMStorageTest::TestClearRecentHistory2_Clear", this, + &CDMStorageTest::TestClearRecentHistory2_Clear); + Expect("test-storage complete"_ns, r.forget()); + + CreateDecryptor(u"http://example1.com"_ns, u"http://example2.com"_ns, false, + "test-storage"_ns); + } + + /** + * 1. Generate some storage data. + * 2. Find the max mtime |t| in $profileDir/gmp/$platform/gmp-fake/storage/. + * 3. Pass |t+1| to clear recent history. + * 4. Check if all directories in $profileDir/gmp/$platform/gmp-fake/id/ and + * $profileDir/gmp/$platform/gmp-fake/storage remain unchanged. + */ + void TestClearRecentHistory3() { + AssertIsOnGMPThread(); + EXPECT_TRUE(IsCDMStorageIsEmpty()); + + // Generate storage data for some site. + nsCOMPtr<nsIRunnable> r = + NewRunnableMethod("CDMStorageTest::TestClearRecentHistory3_Clear", this, + &CDMStorageTest::TestClearRecentHistory3_Clear); + Expect("test-storage complete"_ns, r.forget()); + + CreateDecryptor(u"http://example1.com"_ns, u"http://example2.com"_ns, false, + "test-storage"_ns); + } + + class MaxMTimeFinder { + public: + MaxMTimeFinder() : mMaxTime(0) {} + void operator()(nsIFile* aFile) { + PRTime lastModified; + nsresult rv = aFile->GetLastModifiedTime(&lastModified); + if (NS_SUCCEEDED(rv) && lastModified > mMaxTime) { + mMaxTime = lastModified; + } + EnumerateDir(aFile, *this); + } + PRTime GetResult() const { return mMaxTime; } + + private: + PRTime mMaxTime; + }; + + void TestClearRecentHistory1_Clear() { + MaxMTimeFinder f; + nsresult rv = EnumerateCDMStorageDir("id"_ns, f); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + + nsCOMPtr<nsIRunnable> r = NewRunnableMethod( + "CDMStorageTest::TestClearRecentHistory_CheckEmpty", this, + &CDMStorageTest::TestClearRecentHistory_CheckEmpty); + nsCOMPtr<nsIThread> t(GetGMPThread()); + ClearCDMStorage(r.forget(), t, f.GetResult()); + } + + void TestClearRecentHistory2_Clear() { + MaxMTimeFinder f; + nsresult rv = EnumerateCDMStorageDir("storage"_ns, f); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + + nsCOMPtr<nsIRunnable> r = NewRunnableMethod( + "CDMStorageTest::TestClearRecentHistory_CheckEmpty", this, + &CDMStorageTest::TestClearRecentHistory_CheckEmpty); + nsCOMPtr<nsIThread> t(GetGMPThread()); + ClearCDMStorage(r.forget(), t, f.GetResult()); + } + + void TestClearRecentHistory3_Clear() { + MaxMTimeFinder f; + nsresult rv = EnumerateCDMStorageDir("storage"_ns, f); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + + nsCOMPtr<nsIRunnable> r = NewRunnableMethod( + "CDMStorageTest::TestClearRecentHistory_CheckNonEmpty", this, + &CDMStorageTest::TestClearRecentHistory_CheckNonEmpty); + nsCOMPtr<nsIThread> t(GetGMPThread()); + ClearCDMStorage(r.forget(), t, f.GetResult() + 1); + } + + class FileCounter { + public: + FileCounter() : mCount(0) {} + void operator()(nsIFile* aFile) { ++mCount; } + int GetCount() const { return mCount; } + + private: + int mCount; + }; + + void TestClearRecentHistory_CheckEmpty() { + FileCounter c1; + nsresult rv = EnumerateCDMStorageDir("id"_ns, c1); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + // There should be no files under $profileDir/gmp/$platform/gmp-fake/id/ + EXPECT_EQ(c1.GetCount(), 0); + + FileCounter c2; + rv = EnumerateCDMStorageDir("storage"_ns, c2); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + // There should be no files under + // $profileDir/gmp/$platform/gmp-fake/storage/ + EXPECT_EQ(c2.GetCount(), 0); + + SetFinished(); + } + + void TestClearRecentHistory_CheckNonEmpty() { + FileCounter c1; + nsresult rv = EnumerateCDMStorageDir("id"_ns, c1); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + // There should be one directory under + // $profileDir/gmp/$platform/gmp-fake/id/ + EXPECT_EQ(c1.GetCount(), 1); + + FileCounter c2; + rv = EnumerateCDMStorageDir("storage"_ns, c2); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + // There should be one directory under + // $profileDir/gmp/$platform/gmp-fake/storage/ + EXPECT_EQ(c2.GetCount(), 1); + + SetFinished(); + } + + void TestCrossOriginStorage() { + EXPECT_TRUE(!mCDM); + + // Send the decryptor the message "store recordid $time" + // Wait for the decrytor to send us "stored recordid $time" + auto t = time(0); + nsCString response("stored crossOriginTestRecordId "); + response.AppendInt((int64_t)t); + Expect( + response, + NewRunnableMethod( + "CDMStorageTest::TestCrossOriginStorage_RecordStoredContinuation", + this, + &CDMStorageTest::TestCrossOriginStorage_RecordStoredContinuation)); + + nsCString update("store crossOriginTestRecordId "); + update.AppendInt((int64_t)t); + + // Open decryptor on one, origin, write a record, and test that that + // record can't be read on another origin. + CreateDecryptor(u"http://example3.com"_ns, u"http://example4.com"_ns, false, + update); + } + + void TestCrossOriginStorage_RecordStoredContinuation() { + // Close the old decryptor, and create a new one on a different origin, + // and try to read the record. + Shutdown(); + + Expect(nsLiteralCString( + "retrieve crossOriginTestRecordId succeeded (length 0 bytes)"), + NewRunnableMethod("CDMStorageTest::SetFinished", this, + &CDMStorageTest::SetFinished)); + + CreateDecryptor(u"http://example5.com"_ns, u"http://example6.com"_ns, false, + "retrieve crossOriginTestRecordId"_ns); + } + + void TestPBStorage() { + // Send the decryptor the message "store recordid $time" + // Wait for the decrytor to send us "stored recordid $time" + nsCString response("stored pbdata test-pb-data"); + Expect(response, + NewRunnableMethod( + "CDMStorageTest::TestPBStorage_RecordStoredContinuation", this, + &CDMStorageTest::TestPBStorage_RecordStoredContinuation)); + + // Open decryptor on one, origin, write a record, close decryptor, + // open another, and test that record can be read, close decryptor, + // then send pb-last-context-closed notification, then open decryptor + // and check that it can't read that data; it should have been purged. + CreateDecryptor(u"http://pb1.com"_ns, u"http://pb2.com"_ns, true, + "store pbdata test-pb-data"_ns); + } + + void TestPBStorage_RecordStoredContinuation() { + Shutdown(); + + Expect( + "retrieve pbdata succeeded (length 12 bytes)"_ns, + NewRunnableMethod( + "CDMStorageTest::TestPBStorage_RecordRetrievedContinuation", this, + &CDMStorageTest::TestPBStorage_RecordRetrievedContinuation)); + + CreateDecryptor(u"http://pb1.com"_ns, u"http://pb2.com"_ns, true, + "retrieve pbdata"_ns); + } + + void TestPBStorage_RecordRetrievedContinuation() { + Shutdown(); + SimulatePBModeExit(); + + Expect("retrieve pbdata succeeded (length 0 bytes)"_ns, + NewRunnableMethod("CDMStorageTest::SetFinished", this, + &CDMStorageTest::SetFinished)); + + CreateDecryptor(u"http://pb1.com"_ns, u"http://pb2.com"_ns, true, + "retrieve pbdata"_ns); + } + +#if defined(XP_WIN) + void TestOutputProtection() { + Shutdown(); + + Expect("OP tests completed"_ns, + NewRunnableMethod("CDMStorageTest::SetFinished", this, + &CDMStorageTest::SetFinished)); + + CreateDecryptor(u"http://example15.com"_ns, u"http://example16.com"_ns, + false, "test-op-apis"_ns); + } +#endif + + void TestLongRecordNames() { + constexpr auto longRecordName = + "A_" + "very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_" + "very_very_very_very_very_very_very_very_very_" + "very_very_very_very_very_very_" + "long_record_name"_ns; + + constexpr auto data = "Just_some_arbitrary_data."_ns; + + MOZ_ASSERT(longRecordName.Length() < GMP_MAX_RECORD_NAME_SIZE); + MOZ_ASSERT(longRecordName.Length() > 260); // Windows MAX_PATH + + nsCString response("stored "); + response.Append(longRecordName); + response.AppendLiteral(" "); + response.Append(data); + Expect(response, NewRunnableMethod("CDMStorageTest::SetFinished", this, + &CDMStorageTest::SetFinished)); + + nsCString update("store "); + update.Append(longRecordName); + update.AppendLiteral(" "); + update.Append(data); + CreateDecryptor(u"http://fuz.com"_ns, u"http://baz.com"_ns, false, update); + } + + void Expect(const nsCString& aMessage, + already_AddRefed<nsIRunnable> aContinuation) { + mExpected.AppendElement( + ExpectedMessage(aMessage, std::move(aContinuation))); + } + + void AwaitFinished() { + mozilla::SpinEventLoopUntil([&]() -> bool { return mFinished; }); + mFinished = false; + } + + void ShutdownThen(already_AddRefed<nsIRunnable> aContinuation) { + EXPECT_TRUE(!!mCDM); + if (!mCDM) { + return; + } + EXPECT_FALSE(mNodeId.IsEmpty()); + RefPtr<GMPShutdownObserver> task(new GMPShutdownObserver( + NewRunnableMethod("CDMStorageTest::Shutdown", this, + &CDMStorageTest::Shutdown), + std::move(aContinuation), mNodeId)); + SchedulerGroup::Dispatch(TaskCategory::Other, task.forget()); + } + + void Shutdown() { + if (mCDM) { + mCDM->Shutdown(); + mCDM = nullptr; + mNodeId.Truncate(); + } + } + + void Dummy() {} + + void SetFinished() { + mFinished = true; + Shutdown(); + nsCOMPtr<nsIRunnable> task = NewRunnableMethod( + "CDMStorageTest::Dummy", this, &CDMStorageTest::Dummy); + SchedulerGroup::Dispatch(TaskCategory::Other, task.forget()); + } + + void SessionMessage(const nsACString& aSessionId, uint32_t aMessageType, + const nsTArray<uint8_t>& aMessage) { + MonitorAutoLock mon(mMonitor); + + nsCString msg((const char*)aMessage.Elements(), aMessage.Length()); + EXPECT_TRUE(mExpected.Length() > 0); + bool matches = mExpected[0].mMessage.Equals(msg); + EXPECT_STREQ(mExpected[0].mMessage.get(), msg.get()); + if (mExpected.Length() > 0 && matches) { + nsCOMPtr<nsIRunnable> continuation = mExpected[0].mContinuation; + mExpected.RemoveElementAt(0); + if (continuation) { + NS_DispatchToCurrentThread(continuation); + } + } + } + + void Terminated() { + if (mCDM) { + mCDM->Shutdown(); + mCDM = nullptr; + } + } + + private: + ~CDMStorageTest() = default; + + struct ExpectedMessage { + ExpectedMessage(const nsCString& aMessage, + already_AddRefed<nsIRunnable> aContinuation) + : mMessage(aMessage), mContinuation(aContinuation) {} + nsCString mMessage; + nsCOMPtr<nsIRunnable> mContinuation; + }; + + nsTArray<ExpectedMessage> mExpected; + + RefPtr<nsIRunnable> mSetDecryptorIdContinuation; + + RefPtr<gmp::ChromiumCDMParent> mCDM; + Monitor mMonitor; + Atomic<bool> mFinished; + nsCString mNodeId; + + class CallbackProxy : public ChromiumCDMCallback { + public: + explicit CallbackProxy(CDMStorageTest* aRunner) : mRunner(aRunner) {} + + void SetSessionId(uint32_t aPromiseId, + const nsCString& aSessionId) override {} + + void ResolveLoadSessionPromise(uint32_t aPromiseId, + bool aSuccessful) override {} + + void ResolvePromiseWithKeyStatus(uint32_t aPromiseId, + uint32_t aKeyStatus) override {} + + void ResolvePromise(uint32_t aPromiseId) override {} + + void RejectPromise(uint32_t aPromiseId, ErrorResult&& aError, + const nsCString& aErrorMessage) override {} + + void SessionMessage(const nsACString& aSessionId, uint32_t aMessageType, + nsTArray<uint8_t>&& aMessage) override { + mRunner->SessionMessage(aSessionId, aMessageType, std::move(aMessage)); + } + + void SessionKeysChange( + const nsCString& aSessionId, + nsTArray<mozilla::gmp::CDMKeyInformation>&& aKeysInfo) override {} + + void ExpirationChange(const nsCString& aSessionId, + double aSecondsSinceEpoch) override {} + + void SessionClosed(const nsCString& aSessionId) override {} + + void Terminated() override { mRunner->Terminated(); } + + void Shutdown() override { mRunner->Shutdown(); } + + private: + // Warning: Weak ref. + CDMStorageTest* mRunner; + }; + + UniquePtr<CallbackProxy> mCallback; +}; // class CDMStorageTest + +TEST(GeckoMediaPlugins, CDMStorageGetNodeId) +{ + RefPtr<CDMStorageTest> runner = new CDMStorageTest(); + runner->DoTest(&CDMStorageTest::TestGetNodeId); +} + +TEST(GeckoMediaPlugins, CDMStorageBasic) +{ + RefPtr<CDMStorageTest> runner = new CDMStorageTest(); + runner->DoTest(&CDMStorageTest::TestBasicStorage); +} + +TEST(GeckoMediaPlugins, CDMStorageForgetThisSite) +{ + RefPtr<CDMStorageTest> runner = new CDMStorageTest(); + runner->DoTest(&CDMStorageTest::TestForgetThisSite); +} + +TEST(GeckoMediaPlugins, CDMStorageClearRecentHistory1) +{ + RefPtr<CDMStorageTest> runner = new CDMStorageTest(); + runner->DoTest(&CDMStorageTest::TestClearRecentHistory1); +} + +TEST(GeckoMediaPlugins, CDMStorageClearRecentHistory2) +{ + RefPtr<CDMStorageTest> runner = new CDMStorageTest(); + runner->DoTest(&CDMStorageTest::TestClearRecentHistory2); +} + +TEST(GeckoMediaPlugins, CDMStorageClearRecentHistory3) +{ + RefPtr<CDMStorageTest> runner = new CDMStorageTest(); + runner->DoTest(&CDMStorageTest::TestClearRecentHistory3); +} + +TEST(GeckoMediaPlugins, CDMStorageCrossOrigin) +{ + RefPtr<CDMStorageTest> runner = new CDMStorageTest(); + runner->DoTest(&CDMStorageTest::TestCrossOriginStorage); +} + +TEST(GeckoMediaPlugins, CDMStoragePrivateBrowsing) +{ + RefPtr<CDMStorageTest> runner = new CDMStorageTest(); + runner->DoTest(&CDMStorageTest::TestPBStorage); +} + +#if defined(XP_WIN) +TEST(GeckoMediaPlugins, GMPOutputProtection) +{ + RefPtr<CDMStorageTest> runner = new CDMStorageTest(); + runner->DoTest(&CDMStorageTest::TestOutputProtection); +} +#endif + +TEST(GeckoMediaPlugins, CDMStorageLongRecordNames) +{ + RefPtr<CDMStorageTest> runner = new CDMStorageTest(); + runner->DoTest(&CDMStorageTest::TestLongRecordNames); +} diff --git a/dom/media/gtest/TestDataMutex.cpp b/dom/media/gtest/TestDataMutex.cpp new file mode 100644 index 0000000000..11f3e395c9 --- /dev/null +++ b/dom/media/gtest/TestDataMutex.cpp @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "mozilla/DataMutex.h" +#include "nsTArray.h" + +using mozilla::DataMutex; + +struct A { + void Set(int a) { mValue = a; } + int mValue; +}; + +TEST(DataMutex, Basic) +{ + { + DataMutex<uint32_t> i(1, "1"); + i.Mutex().AssertNotCurrentThreadOwns(); + { + auto x = i.Lock(); + i.Mutex().AssertCurrentThreadOwns(); + *x = 4; + ASSERT_EQ(*x, 4u); + } + i.Mutex().AssertNotCurrentThreadOwns(); + } + { + DataMutex<A> a({4}, "StructA"); + auto x = a.Lock(); + ASSERT_EQ(x->mValue, 4); + x->Set(8); + ASSERT_EQ(x->mValue, 8); + } + { + DataMutex<nsTArray<uint32_t>> _a("array"); + auto a = _a.Lock(); + auto& x = a.ref(); + ASSERT_EQ(x.Length(), 0u); + x.AppendElement(1u); + ASSERT_EQ(x.Length(), 1u); + ASSERT_EQ(x[0], 1u); + } +} diff --git a/dom/media/gtest/TestDecoderBenchmark.cpp b/dom/media/gtest/TestDecoderBenchmark.cpp new file mode 100644 index 0000000000..85091a4946 --- /dev/null +++ b/dom/media/gtest/TestDecoderBenchmark.cpp @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "DecoderBenchmark.h" + +#include "gmock/gmock.h" +#include "gtest/gtest-printers.h" +#include "gtest/gtest.h" + +using ::testing::Return; +using namespace mozilla; + +TEST(DecoderBenchmark, CreateKey) +{ + DecoderBenchmarkInfo info{"video/av1"_ns, 1, 1, 1, 8}; + EXPECT_EQ(KeyUtil::CreateKey(info), + "ResolutionLevel0-FrameRateLevel0-8bit"_ns) + << "Min level"; + + DecoderBenchmarkInfo info1{"video/av1"_ns, 5000, 5000, 100, 8}; + EXPECT_EQ(KeyUtil::CreateKey(info1), + "ResolutionLevel7-FrameRateLevel4-8bit"_ns) + << "Max level"; + + DecoderBenchmarkInfo info2{"video/av1"_ns, 854, 480, 30, 8}; + EXPECT_EQ(KeyUtil::CreateKey(info2), + "ResolutionLevel3-FrameRateLevel2-8bit"_ns) + << "On the top of 4th resolution level"; + + DecoderBenchmarkInfo info3{"video/av1"_ns, 1270, 710, 24, 8}; + EXPECT_EQ(KeyUtil::CreateKey(info3), + "ResolutionLevel4-FrameRateLevel1-8bit"_ns) + << "Closer to 5th resolution level - bellow"; + + DecoderBenchmarkInfo info4{"video/av1"_ns, 1290, 730, 24, 8}; + EXPECT_EQ(KeyUtil::CreateKey(info4), + "ResolutionLevel4-FrameRateLevel1-8bit"_ns) + << "Closer to 5th resolution level - above"; + + DecoderBenchmarkInfo info5{"video/av1"_ns, 854, 480, 20, 8}; + EXPECT_EQ(KeyUtil::CreateKey(info5), + "ResolutionLevel3-FrameRateLevel1-8bit"_ns) + << "Closer to 2nd frame rate level - bellow"; + + DecoderBenchmarkInfo info6{"video/av1"_ns, 854, 480, 26, 8}; + EXPECT_EQ(KeyUtil::CreateKey(info6), + "ResolutionLevel3-FrameRateLevel1-8bit"_ns) + << "Closer to 2nd frame rate level - above"; + + DecoderBenchmarkInfo info7{"video/av1"_ns, 1280, 720, 24, 10}; + EXPECT_EQ(KeyUtil::CreateKey(info7), + "ResolutionLevel4-FrameRateLevel1-non8bit"_ns) + << "Bit depth 10 bits"; + + DecoderBenchmarkInfo info8{"video/av1"_ns, 1280, 720, 24, 12}; + EXPECT_EQ(KeyUtil::CreateKey(info8), + "ResolutionLevel4-FrameRateLevel1-non8bit"_ns) + << "Bit depth 12 bits"; + + DecoderBenchmarkInfo info9{"video/av1"_ns, 1280, 720, 24, 16}; + EXPECT_EQ(KeyUtil::CreateKey(info9), + "ResolutionLevel4-FrameRateLevel1-non8bit"_ns) + << "Bit depth 16 bits"; +} diff --git a/dom/media/gtest/TestDriftCompensation.cpp b/dom/media/gtest/TestDriftCompensation.cpp new file mode 100644 index 0000000000..456da30640 --- /dev/null +++ b/dom/media/gtest/TestDriftCompensation.cpp @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "DriftCompensation.h" +#include "mozilla/SpinEventLoopUntil.h" + +using namespace mozilla; + +class DriftCompensatorTest : public ::testing::Test { + public: + const TrackRate mRate = 44100; + const TimeStamp mStart; + const RefPtr<DriftCompensator> mComp; + + DriftCompensatorTest() + : mStart(TimeStamp::Now()), + mComp(MakeRefPtr<DriftCompensator>(GetCurrentEventTarget(), mRate)) { + mComp->NotifyAudioStart(mStart); + // NotifyAudioStart dispatched a runnable to update the audio mStart time on + // the video thread. Because this is a test, the video thread is the current + // thread. We spin the event loop until we know the mStart time is updated. + { + bool updated = false; + NS_DispatchToCurrentThread( + NS_NewRunnableFunction(__func__, [&] { updated = true; })); + SpinEventLoopUntil([&] { return updated; }); + } + } + + // Past() is half as far from `mStart` as `aNow`. + TimeStamp Past(TimeStamp aNow) { + return mStart + (aNow - mStart) / (int64_t)2; + } + + // Future() is twice as far from `mStart` as `aNow`. + TimeStamp Future(TimeStamp aNow) { return mStart + (aNow - mStart) * 2; } +}; + +TEST_F(DriftCompensatorTest, Initialized) { + EXPECT_EQ(mComp->GetVideoTime(mStart, mStart), mStart); +} + +TEST_F(DriftCompensatorTest, SlowerAudio) { + // 10s of audio took 20 seconds of wall clock to play out + mComp->NotifyAudio(mRate * 10); + TimeStamp now = mStart + TimeDuration::FromSeconds(20); + EXPECT_EQ((mComp->GetVideoTime(now, mStart) - mStart).ToSeconds(), 0.0); + EXPECT_EQ((mComp->GetVideoTime(now, Past(now)) - mStart).ToSeconds(), 5.0); + EXPECT_EQ((mComp->GetVideoTime(now, now) - mStart).ToSeconds(), 10.0); + EXPECT_EQ((mComp->GetVideoTime(now, Future(now)) - mStart).ToSeconds(), 20.0); +} + +TEST_F(DriftCompensatorTest, NoDrift) { + // 10s of audio took 10 seconds of wall clock to play out + mComp->NotifyAudio(mRate * 10); + TimeStamp now = mStart + TimeDuration::FromSeconds(10); + EXPECT_EQ((mComp->GetVideoTime(now, mStart) - mStart).ToSeconds(), 0.0); + EXPECT_EQ((mComp->GetVideoTime(now, Past(now)) - mStart).ToSeconds(), 5.0); + EXPECT_EQ((mComp->GetVideoTime(now, now) - mStart).ToSeconds(), 10.0); + EXPECT_EQ((mComp->GetVideoTime(now, Future(now)) - mStart).ToSeconds(), 20.0); +} + +TEST_F(DriftCompensatorTest, NoProgress) { + // 10s of audio took 0 seconds of wall clock to play out + mComp->NotifyAudio(mRate * 10); + TimeStamp now = mStart; + TimeStamp future = mStart + TimeDuration::FromSeconds(5); + EXPECT_EQ((mComp->GetVideoTime(now, mStart) - mStart).ToSeconds(), 0.0); + EXPECT_EQ((mComp->GetVideoTime(now, future) - mStart).ToSeconds(), 5.0); +} + +TEST_F(DriftCompensatorTest, FasterAudio) { + // 20s of audio took 10 seconds of wall clock to play out + mComp->NotifyAudio(mRate * 20); + TimeStamp now = mStart + TimeDuration::FromSeconds(10); + EXPECT_EQ((mComp->GetVideoTime(now, mStart) - mStart).ToSeconds(), 0.0); + EXPECT_EQ((mComp->GetVideoTime(now, Past(now)) - mStart).ToSeconds(), 10.0); + EXPECT_EQ((mComp->GetVideoTime(now, now) - mStart).ToSeconds(), 20.0); + EXPECT_EQ((mComp->GetVideoTime(now, Future(now)) - mStart).ToSeconds(), 40.0); +} diff --git a/dom/media/gtest/TestDynamicResampler.cpp b/dom/media/gtest/TestDynamicResampler.cpp new file mode 100644 index 0000000000..a84b0be3ee --- /dev/null +++ b/dom/media/gtest/TestDynamicResampler.cpp @@ -0,0 +1,1469 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gmock/gmock.h" +#include "gtest/gtest-printers.h" +#include "gtest/gtest.h" + +#include "DynamicResampler.h" + +using namespace mozilla; + +TEST(TestDynamicResampler, SameRates_Float1) +{ + const uint32_t in_frames = 100; + const uint32_t out_frames = 100; + uint32_t channels = 2; + uint32_t in_rate = 44100; + uint32_t out_rate = 44100; + + DynamicResampler dr(in_rate, out_rate); + dr.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + EXPECT_EQ(dr.GetOutRate(), out_rate); + EXPECT_EQ(dr.GetChannels(), channels); + + // float in_ch1[] = {.1, .2, .3, .4, .5, .6, .7, .8, .9, 1.0}; + // float in_ch2[] = {.1, .2, .3, .4, .5, .6, .7, .8, .9, 1.0}; + float in_ch1[in_frames] = {}; + float in_ch2[in_frames] = {}; + AutoTArray<const float*, 2> in_buffer; + in_buffer.AppendElements(channels); + in_buffer[0] = in_ch1; + in_buffer[1] = in_ch2; + + float out_ch1[out_frames] = {}; + float out_ch2[out_frames] = {}; + + // Warm up with zeros + dr.AppendInput(in_buffer, in_frames); + uint32_t out_frames_used = out_frames; + bool rv = dr.Resample(out_ch1, &out_frames_used, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames_used, out_frames); + rv = dr.Resample(out_ch2, &out_frames_used, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames_used, out_frames); + for (uint32_t i = 0; i < out_frames; ++i) { + EXPECT_FLOAT_EQ(in_ch1[i], out_ch1[i]); + EXPECT_FLOAT_EQ(in_ch2[i], out_ch2[i]); + } + + // Continue with non zero + for (uint32_t i = 0; i < in_frames; ++i) { + in_ch1[i] = in_ch2[i] = 0.01f * i; + } + dr.AppendInput(in_buffer, in_frames); + out_frames_used = out_frames; + rv = dr.Resample(out_ch1, &out_frames_used, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames_used, out_frames); + rv = dr.Resample(out_ch2, &out_frames_used, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames_used, out_frames); + for (uint32_t i = 0; i < out_frames; ++i) { + EXPECT_FLOAT_EQ(in_ch1[i], out_ch1[i]); + EXPECT_FLOAT_EQ(in_ch2[i], out_ch2[i]); + } + + // No more frames in the input buffer + rv = dr.Resample(out_ch1, &out_frames_used, 0); + EXPECT_FALSE(rv); + EXPECT_EQ(out_frames_used, 0u); + out_frames_used = 2; + rv = dr.Resample(out_ch2, &out_frames_used, 1); + EXPECT_FALSE(rv); + EXPECT_EQ(out_frames_used, 0u); +} + +TEST(TestDynamicResampler, SameRates_Short1) +{ + uint32_t in_frames = 2; + uint32_t out_frames = 2; + uint32_t channels = 2; + uint32_t in_rate = 44100; + uint32_t out_rate = 44100; + + DynamicResampler dr(in_rate, out_rate); + dr.SetSampleFormat(AUDIO_FORMAT_S16); + EXPECT_EQ(dr.GetOutRate(), out_rate); + EXPECT_EQ(dr.GetChannels(), channels); + + short in_ch1[] = {1, 2, 3}; + short in_ch2[] = {4, 5, 6}; + AutoTArray<const short*, 2> in_buffer; + in_buffer.AppendElements(channels); + in_buffer[0] = in_ch1; + in_buffer[1] = in_ch2; + + short out_ch1[3] = {}; + short out_ch2[3] = {}; + + dr.AppendInput(in_buffer, in_frames); + bool rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 2u); + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 2u); + for (uint32_t i = 0; i < out_frames; ++i) { + EXPECT_EQ(in_ch1[i], out_ch1[i]); + EXPECT_EQ(in_ch2[i], out_ch2[i]); + } + + // No more frames in the input buffer + rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_FALSE(rv); + EXPECT_EQ(out_frames, 0u); + out_frames = 2; + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_FALSE(rv); + EXPECT_EQ(out_frames, 0u); +} + +TEST(TestDynamicResampler, SameRates_Float2) +{ + uint32_t in_frames = 3; + uint32_t out_frames = 2; + uint32_t channels = 2; + uint32_t in_rate = 44100; + uint32_t out_rate = 44100; + + DynamicResampler dr(in_rate, out_rate); + dr.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + + float in_ch1[] = {0.1, 0.2, 0.3}; + float in_ch2[] = {0.4, 0.5, 0.6}; + AutoTArray<const float*, 2> in_buffer; + in_buffer.AppendElements(channels); + in_buffer[0] = in_ch1; + in_buffer[1] = in_ch2; + + float out_ch1[3] = {}; + float out_ch2[3] = {}; + + dr.AppendInput(in_buffer, in_frames); + bool rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 2u); + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 2u); + for (uint32_t i = 0; i < out_frames; ++i) { + EXPECT_FLOAT_EQ(in_ch1[i], out_ch1[i]); + EXPECT_FLOAT_EQ(in_ch2[i], out_ch2[i]); + } + + out_frames = 1; + rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 1u); + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 1u); + for (uint32_t i = 0; i < out_frames; ++i) { + EXPECT_FLOAT_EQ(in_ch1[i + 2], out_ch1[i]); + EXPECT_FLOAT_EQ(in_ch2[i + 2], out_ch2[i]); + } + + // No more frames, the input buffer has drained + rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_FALSE(rv); + EXPECT_EQ(out_frames, 0u); + out_frames = 1; + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_FALSE(rv); + EXPECT_EQ(out_frames, 0u); +} + +TEST(TestDynamicResampler, SameRates_Short2) +{ + uint32_t in_frames = 3; + uint32_t out_frames = 2; + uint32_t channels = 2; + uint32_t in_rate = 44100; + uint32_t out_rate = 44100; + + DynamicResampler dr(in_rate, out_rate); + dr.SetSampleFormat(AUDIO_FORMAT_S16); + + short in_ch1[] = {1, 2, 3}; + short in_ch2[] = {4, 5, 6}; + AutoTArray<const short*, 2> in_buffer; + in_buffer.AppendElements(channels); + in_buffer[0] = in_ch1; + in_buffer[1] = in_ch2; + + short out_ch1[3] = {}; + short out_ch2[3] = {}; + + dr.AppendInput(in_buffer, in_frames); + bool rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 2u); + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 2u); + for (uint32_t i = 0; i < out_frames; ++i) { + EXPECT_EQ(in_ch1[i], out_ch1[i]); + EXPECT_EQ(in_ch2[i], out_ch2[i]); + } + + out_frames = 1; + rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 1u); + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 1u); + for (uint32_t i = 0; i < out_frames; ++i) { + EXPECT_EQ(in_ch1[i + 2], out_ch1[i]); + EXPECT_EQ(in_ch2[i + 2], out_ch2[i]); + } + + // No more frames, the input buffer has drained + rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_FALSE(rv); + EXPECT_EQ(out_frames, 0u); + out_frames = 1; + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_FALSE(rv); + EXPECT_EQ(out_frames, 0u); +} + +TEST(TestDynamicResampler, SameRates_Float3) +{ + uint32_t in_frames = 2; + uint32_t out_frames = 3; + uint32_t channels = 2; + uint32_t in_rate = 44100; + uint32_t out_rate = 44100; + + DynamicResampler dr(in_rate, out_rate); + dr.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + + float in_ch1[] = {0.1, 0.2, 0.3}; + float in_ch2[] = {0.4, 0.5, 0.6}; + AutoTArray<const float*, 2> in_buffer; + in_buffer.AppendElements(channels); + in_buffer[0] = in_ch1; + in_buffer[1] = in_ch2; + + float out_ch1[3] = {}; + float out_ch2[3] = {}; + + // Not enough frames in the input buffer + dr.AppendInput(in_buffer, in_frames); + bool rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_FALSE(rv); + EXPECT_EQ(out_frames, 0u); + out_frames = 3; + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_FALSE(rv); + EXPECT_EQ(out_frames, 0u); + + // Add one more frame + in_buffer[0] = in_ch1 + 2; + in_buffer[1] = in_ch2 + 2; + dr.AppendInput(in_buffer, 1); + out_frames = 3; + rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 3u); + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 3u); + for (uint32_t i = 0; i < out_frames; ++i) { + EXPECT_FLOAT_EQ(in_ch1[i], out_ch1[i]); + EXPECT_FLOAT_EQ(in_ch2[i], out_ch2[i]); + } +} + +TEST(TestDynamicResampler, SameRates_Short3) +{ + uint32_t in_frames = 2; + uint32_t out_frames = 3; + uint32_t channels = 2; + uint32_t in_rate = 44100; + uint32_t out_rate = 44100; + + DynamicResampler dr(in_rate, out_rate); + dr.SetSampleFormat(AUDIO_FORMAT_S16); + + short in_ch1[] = {1, 2, 3}; + short in_ch2[] = {4, 5, 6}; + AutoTArray<const short*, 2> in_buffer; + in_buffer.AppendElements(channels); + in_buffer[0] = in_ch1; + in_buffer[1] = in_ch2; + + short out_ch1[3] = {}; + short out_ch2[3] = {}; + + // Not enough frames in the input buffer + dr.AppendInput(in_buffer, in_frames); + bool rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_FALSE(rv); + EXPECT_EQ(out_frames, 0u); + out_frames = 3; + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_FALSE(rv); + EXPECT_EQ(out_frames, 0u); + + // Add one more frame + in_buffer[0] = in_ch1 + 2; + in_buffer[1] = in_ch2 + 2; + dr.AppendInput(in_buffer, 1); + out_frames = 3; + rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 3u); + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 3u); + for (uint32_t i = 0; i < out_frames; ++i) { + EXPECT_EQ(in_ch1[i], out_ch1[i]); + EXPECT_EQ(in_ch2[i], out_ch2[i]); + } +} + +TEST(TestDynamicResampler, UpdateOutRate_Float) +{ + uint32_t in_frames = 10; + uint32_t out_frames = 40; + uint32_t channels = 2; + uint32_t in_rate = 24000; + uint32_t out_rate = 48000; + + uint32_t pre_buffer = 20; + + DynamicResampler dr(in_rate, out_rate, pre_buffer); + dr.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + EXPECT_EQ(dr.GetOutRate(), out_rate); + EXPECT_EQ(dr.GetChannels(), channels); + + float in_ch1[10] = {}; + float in_ch2[10] = {}; + for (uint32_t i = 0; i < in_frames; ++i) { + in_ch1[i] = in_ch2[i] = 0.01f * i; + } + AutoTArray<const float*, 2> in_buffer; + in_buffer.AppendElements(channels); + in_buffer[0] = in_ch1; + in_buffer[1] = in_ch2; + + float out_ch1[40] = {}; + float out_ch2[40] = {}; + + dr.AppendInput(in_buffer, in_frames); + bool rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 40u); + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 40u); + for (uint32_t i = 0; i < out_frames; ++i) { + // Only pre buffered data reach output + EXPECT_FLOAT_EQ(out_ch1[i], 0.0); + EXPECT_FLOAT_EQ(out_ch2[i], 0.0); + } + + // Update out rate + out_rate = 44100; + dr.UpdateResampler(out_rate, channels); + EXPECT_EQ(dr.GetOutRate(), out_rate); + EXPECT_EQ(dr.GetChannels(), channels); + out_frames = in_frames * out_rate / in_rate; + EXPECT_EQ(out_frames, 18u); + // Even if we provide no input if we have enough buffered input, we can create + // output + rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 18u); + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 18u); +} + +TEST(TestDynamicResampler, UpdateOutRate_Short) +{ + uint32_t in_frames = 10; + uint32_t out_frames = 40; + uint32_t channels = 2; + uint32_t in_rate = 24000; + uint32_t out_rate = 48000; + + uint32_t pre_buffer = 20; + + DynamicResampler dr(in_rate, out_rate, pre_buffer); + dr.SetSampleFormat(AUDIO_FORMAT_S16); + EXPECT_EQ(dr.GetOutRate(), out_rate); + EXPECT_EQ(dr.GetChannels(), channels); + + short in_ch1[10] = {}; + short in_ch2[10] = {}; + for (uint32_t i = 0; i < in_frames; ++i) { + in_ch1[i] = in_ch2[i] = i; + } + AutoTArray<const short*, 2> in_buffer; + in_buffer.AppendElements(channels); + in_buffer[0] = in_ch1; + in_buffer[1] = in_ch2; + + short out_ch1[40] = {}; + short out_ch2[40] = {}; + + dr.AppendInput(in_buffer, in_frames); + bool rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 40u); + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 40u); + for (uint32_t i = 0; i < out_frames; ++i) { + // Only pre buffered data reach output + EXPECT_EQ(out_ch1[i], 0.0); + EXPECT_EQ(out_ch2[i], 0.0); + } + + // Update out rate + out_rate = 44100; + dr.UpdateResampler(out_rate, channels); + EXPECT_EQ(dr.GetOutRate(), out_rate); + EXPECT_EQ(dr.GetChannels(), channels); + out_frames = in_frames * out_rate / in_rate; + EXPECT_EQ(out_frames, 18u); + // Even if we provide no input if we have enough buffered input, we can create + // output + rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 18u); + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 18u); +} + +TEST(TestDynamicResampler, BigRangeOutRates_Float) +{ + uint32_t in_frames = 10; + uint32_t out_frames = 10; + uint32_t channels = 2; + uint32_t in_rate = 44100; + uint32_t out_rate = 44100; + uint32_t pre_buffer = 20; + + DynamicResampler dr(in_rate, out_rate, pre_buffer); + dr.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + + const uint32_t in_capacity = 40; + float in_ch1[in_capacity] = {}; + float in_ch2[in_capacity] = {}; + for (uint32_t i = 0; i < in_capacity; ++i) { + in_ch1[i] = in_ch2[i] = 0.01f * i; + } + AutoTArray<const float*, 2> in_buffer; + in_buffer.AppendElements(channels); + in_buffer[0] = in_ch1; + in_buffer[1] = in_ch2; + + const uint32_t out_capacity = 1000; + float out_ch1[out_capacity] = {}; + float out_ch2[out_capacity] = {}; + + for (uint32_t rate = 10000; rate < 90000; ++rate) { + out_rate = rate; + dr.UpdateResampler(out_rate, channels); + EXPECT_EQ(dr.GetOutRate(), out_rate); + EXPECT_EQ(dr.GetChannels(), channels); + in_frames = 20; // more than we need + out_frames = in_frames * out_rate / in_rate; + uint32_t expected_out_frames = out_frames; + for (uint32_t y = 0; y < 2; ++y) { + dr.AppendInput(in_buffer, in_frames); + bool rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, expected_out_frames); + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, expected_out_frames); + } + } +} + +TEST(TestDynamicResampler, BigRangeOutRates_Short) +{ + uint32_t in_frames = 10; + uint32_t out_frames = 10; + uint32_t channels = 2; + uint32_t in_rate = 44100; + uint32_t out_rate = 44100; + uint32_t pre_buffer = 20; + + DynamicResampler dr(in_rate, out_rate, pre_buffer); + dr.SetSampleFormat(AUDIO_FORMAT_S16); + + const uint32_t in_capacity = 40; + short in_ch1[in_capacity] = {}; + short in_ch2[in_capacity] = {}; + for (uint32_t i = 0; i < in_capacity; ++i) { + in_ch1[i] = in_ch2[i] = i; + } + AutoTArray<const short*, 2> in_buffer; + in_buffer.AppendElements(channels); + in_buffer[0] = in_ch1; + in_buffer[1] = in_ch2; + + const uint32_t out_capacity = 1000; + short out_ch1[out_capacity] = {}; + short out_ch2[out_capacity] = {}; + + for (uint32_t rate = 10000; rate < 90000; ++rate) { + out_rate = rate; + dr.UpdateResampler(out_rate, channels); + in_frames = 20; // more than we need + out_frames = in_frames * out_rate / in_rate; + uint32_t expected_out_frames = out_frames; + for (uint32_t y = 0; y < 2; ++y) { + dr.AppendInput(in_buffer, in_frames); + bool rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, expected_out_frames); + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, expected_out_frames); + } + } +} + +TEST(TestDynamicResampler, UpdateChannels_Float) +{ + uint32_t in_frames = 10; + uint32_t out_frames = 10; + uint32_t channels = 2; + uint32_t in_rate = 44100; + uint32_t out_rate = 48000; + + DynamicResampler dr(in_rate, out_rate); + dr.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + + float in_ch1[10] = {}; + float in_ch2[10] = {}; + for (uint32_t i = 0; i < in_frames; ++i) { + in_ch1[i] = in_ch2[i] = 0.01f * i; + } + AutoTArray<const float*, 2> in_buffer; + in_buffer.AppendElements(channels); + in_buffer[0] = in_ch1; + in_buffer[1] = in_ch2; + + float out_ch1[10] = {}; + float out_ch2[10] = {}; + + dr.AppendInput(in_buffer, in_frames); + bool rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 10u); + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 10u); + + // Add 3rd channel + dr.UpdateResampler(out_rate, 3); + EXPECT_EQ(dr.GetOutRate(), out_rate); + EXPECT_EQ(dr.GetChannels(), 3u); + + float in_ch3[10] = {}; + for (uint32_t i = 0; i < in_frames; ++i) { + in_ch3[i] = 0.01f * i; + } + in_buffer.AppendElement(); + in_buffer[2] = in_ch3; + float out_ch3[10] = {}; + + dr.AppendInput(in_buffer, in_frames); + + rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 10u); + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 10u); + rv = dr.Resample(out_ch3, &out_frames, 2); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 10u); + + float in_ch4[10] = {}; + for (uint32_t i = 0; i < in_frames; ++i) { + in_ch3[i] = 0.01f * i; + } + in_buffer.AppendElement(); + in_buffer[3] = in_ch4; + float out_ch4[10] = {}; + + dr.UpdateResampler(out_rate, 4); + EXPECT_EQ(dr.GetOutRate(), out_rate); + EXPECT_EQ(dr.GetChannels(), 4u); + dr.AppendInput(in_buffer, in_frames); + + rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 10u); + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 10u); + rv = dr.Resample(out_ch3, &out_frames, 2); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 10u); + rv = dr.Resample(out_ch4, &out_frames, 3); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 10u); +} + +TEST(TestDynamicResampler, UpdateChannels_Short) +{ + uint32_t in_frames = 10; + uint32_t out_frames = 10; + uint32_t channels = 2; + uint32_t in_rate = 44100; + uint32_t out_rate = 48000; + + DynamicResampler dr(in_rate, out_rate); + dr.SetSampleFormat(AUDIO_FORMAT_S16); + + short in_ch1[10] = {}; + short in_ch2[10] = {}; + for (uint32_t i = 0; i < in_frames; ++i) { + in_ch1[i] = in_ch2[i] = i; + } + AutoTArray<const short*, 2> in_buffer; + in_buffer.AppendElements(channels); + in_buffer[0] = in_ch1; + in_buffer[1] = in_ch2; + + short out_ch1[10] = {}; + short out_ch2[10] = {}; + + dr.AppendInput(in_buffer, in_frames); + bool rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 10u); + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 10u); + + // Add 3rd channel + dr.UpdateResampler(out_rate, 3); + EXPECT_EQ(dr.GetOutRate(), out_rate); + EXPECT_EQ(dr.GetChannels(), 3u); + + short in_ch3[10] = {}; + for (uint32_t i = 0; i < in_frames; ++i) { + in_ch3[i] = i; + } + in_buffer.AppendElement(); + in_buffer[2] = in_ch3; + short out_ch3[10] = {}; + + dr.AppendInput(in_buffer, in_frames); + + rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 10u); + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 10u); + rv = dr.Resample(out_ch3, &out_frames, 2); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 10u); + + // Check update with AudioSegment + short in_ch4[10] = {}; + for (uint32_t i = 0; i < in_frames; ++i) { + in_ch3[i] = i; + } + in_buffer.AppendElement(); + in_buffer[3] = in_ch4; + short out_ch4[10] = {}; + + dr.UpdateResampler(out_rate, 4); + EXPECT_EQ(dr.GetOutRate(), out_rate); + EXPECT_EQ(dr.GetChannels(), 4u); + dr.AppendInput(in_buffer, in_frames); + + rv = dr.Resample(out_ch1, &out_frames, 0); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 10u); + rv = dr.Resample(out_ch2, &out_frames, 1); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 10u); + rv = dr.Resample(out_ch3, &out_frames, 2); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 10u); + rv = dr.Resample(out_ch4, &out_frames, 3); + EXPECT_TRUE(rv); + EXPECT_EQ(out_frames, 10u); +} + +TEST(TestAudioChunkList, Basic1) +{ + AudioChunkList list(256, 2); + list.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + EXPECT_EQ(list.ChunkCapacity(), 128u); + EXPECT_EQ(list.TotalCapacity(), 256u); + + AudioChunk& c1 = list.GetNext(); + float* c1_ch1 = c1.ChannelDataForWrite<float>(0); + float* c1_ch2 = c1.ChannelDataForWrite<float>(1); + EXPECT_EQ(c1.mBufferFormat, AUDIO_FORMAT_FLOAT32); + for (uint32_t i = 0; i < list.ChunkCapacity(); ++i) { + c1_ch1[i] = c1_ch2[i] = 0.01f * static_cast<float>(i); + } + AudioChunk& c2 = list.GetNext(); + EXPECT_EQ(c2.mBufferFormat, AUDIO_FORMAT_FLOAT32); + EXPECT_NE(c1.mBuffer.get(), c2.mBuffer.get()); + AudioChunk& c3 = list.GetNext(); + EXPECT_EQ(c3.mBufferFormat, AUDIO_FORMAT_FLOAT32); + // Cycle + EXPECT_EQ(c1.mBuffer.get(), c3.mBuffer.get()); + float* c3_ch1 = c3.ChannelDataForWrite<float>(0); + float* c3_ch2 = c3.ChannelDataForWrite<float>(1); + for (uint32_t i = 0; i < list.ChunkCapacity(); ++i) { + EXPECT_FLOAT_EQ(c1_ch1[i], c3_ch1[i]); + EXPECT_FLOAT_EQ(c1_ch2[i], c3_ch2[i]); + } +} + +TEST(TestAudioChunkList, Basic2) +{ + AudioChunkList list(256, 2); + list.SetSampleFormat(AUDIO_FORMAT_S16); + EXPECT_EQ(list.ChunkCapacity(), 256u); + EXPECT_EQ(list.TotalCapacity(), 512u); + + AudioChunk& c1 = list.GetNext(); + EXPECT_EQ(c1.mBufferFormat, AUDIO_FORMAT_S16); + short* c1_ch1 = c1.ChannelDataForWrite<short>(0); + short* c1_ch2 = c1.ChannelDataForWrite<short>(1); + for (uint32_t i = 0; i < list.ChunkCapacity(); ++i) { + c1_ch1[i] = c1_ch2[i] = static_cast<short>(i); + } + AudioChunk& c2 = list.GetNext(); + EXPECT_EQ(c2.mBufferFormat, AUDIO_FORMAT_S16); + EXPECT_NE(c1.mBuffer.get(), c2.mBuffer.get()); + AudioChunk& c3 = list.GetNext(); + EXPECT_EQ(c3.mBufferFormat, AUDIO_FORMAT_S16); + AudioChunk& c4 = list.GetNext(); + EXPECT_EQ(c4.mBufferFormat, AUDIO_FORMAT_S16); + // Cycle + AudioChunk& c5 = list.GetNext(); + EXPECT_EQ(c5.mBufferFormat, AUDIO_FORMAT_S16); + EXPECT_EQ(c1.mBuffer.get(), c5.mBuffer.get()); + short* c5_ch1 = c5.ChannelDataForWrite<short>(0); + short* c5_ch2 = c5.ChannelDataForWrite<short>(1); + for (uint32_t i = 0; i < list.ChunkCapacity(); ++i) { + EXPECT_EQ(c1_ch1[i], c5_ch1[i]); + EXPECT_EQ(c1_ch2[i], c5_ch2[i]); + } +} + +TEST(TestAudioChunkList, Basic3) +{ + AudioChunkList list(260, 2); + list.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + EXPECT_EQ(list.ChunkCapacity(), 128u); + EXPECT_EQ(list.TotalCapacity(), 256u + 128u); + + AudioChunk& c1 = list.GetNext(); + AudioChunk& c2 = list.GetNext(); + EXPECT_NE(c1.mBuffer.get(), c2.mBuffer.get()); + AudioChunk& c3 = list.GetNext(); + EXPECT_NE(c1.mBuffer.get(), c3.mBuffer.get()); + AudioChunk& c4 = list.GetNext(); + EXPECT_EQ(c1.mBuffer.get(), c4.mBuffer.get()); +} + +TEST(TestAudioChunkList, Basic4) +{ + AudioChunkList list(260, 2); + list.SetSampleFormat(AUDIO_FORMAT_S16); + EXPECT_EQ(list.ChunkCapacity(), 256u); + EXPECT_EQ(list.TotalCapacity(), 512u + 256u); + + AudioChunk& c1 = list.GetNext(); + AudioChunk& c2 = list.GetNext(); + EXPECT_NE(c1.mBuffer.get(), c2.mBuffer.get()); + AudioChunk& c3 = list.GetNext(); + EXPECT_NE(c1.mBuffer.get(), c3.mBuffer.get()); + AudioChunk& c4 = list.GetNext(); + EXPECT_EQ(c1.mBuffer.get(), c4.mBuffer.get()); +} + +TEST(TestAudioChunkList, UpdateChannels) +{ + AudioChunkList list(256, 2); + list.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + + AudioChunk& c1 = list.GetNext(); + AudioChunk& c2 = list.GetNext(); + EXPECT_EQ(c1.ChannelCount(), 2u); + EXPECT_EQ(c2.ChannelCount(), 2u); + + // Update to Quad + list.Update(4); + + AudioChunk& c3 = list.GetNext(); + AudioChunk& c4 = list.GetNext(); + EXPECT_EQ(c3.ChannelCount(), 4u); + EXPECT_EQ(c4.ChannelCount(), 4u); +} + +TEST(TestAudioChunkList, UpdateBetweenMonoAndStereo) +{ + AudioChunkList list(256, 2); + list.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + + AudioChunk& c1 = list.GetNext(); + float* c1_ch1 = c1.ChannelDataForWrite<float>(0); + float* c1_ch2 = c1.ChannelDataForWrite<float>(1); + for (uint32_t i = 0; i < list.ChunkCapacity(); ++i) { + c1_ch1[i] = c1_ch2[i] = 0.01f * static_cast<float>(i); + } + + AudioChunk& c2 = list.GetNext(); + EXPECT_EQ(c1.ChannelCount(), 2u); + EXPECT_EQ(c2.ChannelCount(), 2u); + + // Downmix to mono + list.Update(1); + + AudioChunk& c3 = list.GetNext(); + float* c3_ch1 = c3.ChannelDataForWrite<float>(0); + for (uint32_t i = 0; i < list.ChunkCapacity(); ++i) { + EXPECT_FLOAT_EQ(c3_ch1[i], c1_ch1[i]); + } + + AudioChunk& c4 = list.GetNext(); + EXPECT_EQ(c3.ChannelCount(), 1u); + EXPECT_EQ(c4.ChannelCount(), 1u); + EXPECT_EQ(static_cast<SharedChannelArrayBuffer<float>*>(c3.mBuffer.get()) + ->mBuffers[0] + .Length(), + list.ChunkCapacity()); + + // Upmix to stereo + list.Update(2); + + AudioChunk& c5 = list.GetNext(); + AudioChunk& c6 = list.GetNext(); + EXPECT_EQ(c5.ChannelCount(), 2u); + EXPECT_EQ(c6.ChannelCount(), 2u); + EXPECT_EQ(static_cast<SharedChannelArrayBuffer<float>*>(c5.mBuffer.get()) + ->mBuffers[0] + .Length(), + list.ChunkCapacity()); + EXPECT_EQ(static_cast<SharedChannelArrayBuffer<float>*>(c5.mBuffer.get()) + ->mBuffers[1] + .Length(), + list.ChunkCapacity()); + + // Downmix to mono + list.Update(1); + + AudioChunk& c7 = list.GetNext(); + float* c7_ch1 = c7.ChannelDataForWrite<float>(0); + for (uint32_t i = 0; i < list.ChunkCapacity(); ++i) { + EXPECT_FLOAT_EQ(c7_ch1[i], c1_ch1[i]); + } + + AudioChunk& c8 = list.GetNext(); + EXPECT_EQ(c7.ChannelCount(), 1u); + EXPECT_EQ(c8.ChannelCount(), 1u); + EXPECT_EQ(static_cast<SharedChannelArrayBuffer<float>*>(c7.mBuffer.get()) + ->mBuffers[0] + .Length(), + list.ChunkCapacity()); +} + +TEST(TestAudioChunkList, ConsumeAndForget) +{ + AudioSegment s; + AudioChunkList list(256, 2); + list.SetSampleFormat(AUDIO_FORMAT_FLOAT32); + + AudioChunk& c1 = list.GetNext(); + AudioChunk tmp = c1; + s.AppendAndConsumeChunk(&tmp); + EXPECT_FALSE(c1.mBuffer.get() == nullptr); + EXPECT_EQ(c1.ChannelData<float>().Length(), 2u); + + AudioChunk& c2 = list.GetNext(); + tmp = c2; + s.AppendAndConsumeChunk(&tmp); + EXPECT_FALSE(c2.mBuffer.get() == nullptr); + EXPECT_EQ(c2.ChannelData<float>().Length(), 2u); + + s.ForgetUpTo(256); + list.GetNext(); + list.GetNext(); +} + +template <class T> +AudioChunk CreateAudioChunk(uint32_t aFrames, uint32_t aChannels, + AudioSampleFormat aSampleFormat) { + AudioChunk chunk; + nsTArray<nsTArray<T>> buffer; + buffer.AppendElements(aChannels); + + nsTArray<const T*> bufferPtrs; + bufferPtrs.AppendElements(aChannels); + + for (uint32_t i = 0; i < aChannels; ++i) { + T* ptr = buffer[i].AppendElements(aFrames); + bufferPtrs[i] = ptr; + for (uint32_t j = 0; j < aFrames; ++j) { + if (aSampleFormat == AUDIO_FORMAT_FLOAT32) { + ptr[j] = 0.01 * j; + } else { + ptr[j] = j; + } + } + } + + chunk.mBuffer = new mozilla::SharedChannelArrayBuffer(std::move(buffer)); + chunk.mBufferFormat = aSampleFormat; + chunk.mChannelData.AppendElements(aChannels); + for (uint32_t i = 0; i < aChannels; ++i) { + chunk.mChannelData[i] = bufferPtrs[i]; + } + chunk.mDuration = aFrames; + return chunk; +} + +template <class T> +AudioSegment CreateAudioSegment(uint32_t aFrames, uint32_t aChannels, + AudioSampleFormat aSampleFormat) { + AudioSegment segment; + AudioChunk chunk = CreateAudioChunk<T>(aFrames, aChannels, aSampleFormat); + segment.AppendAndConsumeChunk(&chunk); + return segment; +} + +TEST(TestAudioResampler, OutAudioSegment_Float) +{ + uint32_t in_frames = 10; + uint32_t out_frames = 40; + uint32_t channels = 2; + uint32_t in_rate = 24000; + uint32_t out_rate = 48000; + + uint32_t pre_buffer = 21; + + AudioResampler dr(in_rate, out_rate, pre_buffer); + + AudioSegment inSegment = + CreateAudioSegment<float>(in_frames, channels, AUDIO_FORMAT_FLOAT32); + dr.AppendInput(inSegment); + + AudioSegment s = dr.Resample(out_frames); + EXPECT_EQ(s.GetDuration(), 40); + EXPECT_EQ(s.GetType(), MediaSegment::AUDIO); + EXPECT_TRUE(!s.IsNull()); + EXPECT_TRUE(!s.IsEmpty()); + + for (AudioSegment::ChunkIterator ci(s); !ci.IsEnded(); ci.Next()) { + AudioChunk& c = *ci; + EXPECT_EQ(c.ChannelCount(), 2u); + for (uint32_t i = 0; i < out_frames; ++i) { + // Only pre buffered data reach output + EXPECT_FLOAT_EQ(c.ChannelData<float>()[0][i], 0.0); + EXPECT_FLOAT_EQ(c.ChannelData<float>()[1][i], 0.0); + } + } + + // Update out rate + out_rate = 44100; + dr.UpdateOutRate(out_rate); + out_frames = in_frames * out_rate / in_rate; + EXPECT_EQ(out_frames, 18u); + // Even if we provide no input if we have enough buffered input, we can create + // output + AudioSegment s1 = dr.Resample(out_frames); + EXPECT_EQ(s1.GetDuration(), out_frames); + EXPECT_EQ(s1.GetType(), MediaSegment::AUDIO); + EXPECT_TRUE(!s1.IsNull()); + EXPECT_TRUE(!s1.IsEmpty()); +} + +TEST(TestAudioResampler, OutAudioSegment_Short) +{ + uint32_t in_frames = 10; + uint32_t out_frames = 40; + uint32_t channels = 2; + uint32_t in_rate = 24000; + uint32_t out_rate = 48000; + + uint32_t pre_buffer = 21; + + AudioResampler dr(in_rate, out_rate, pre_buffer); + + AudioSegment inSegment = + CreateAudioSegment<short>(in_frames, channels, AUDIO_FORMAT_S16); + dr.AppendInput(inSegment); + + AudioSegment s = dr.Resample(out_frames); + EXPECT_EQ(s.GetDuration(), 40); + EXPECT_EQ(s.GetType(), MediaSegment::AUDIO); + EXPECT_TRUE(!s.IsNull()); + EXPECT_TRUE(!s.IsEmpty()); + + for (AudioSegment::ChunkIterator ci(s); !ci.IsEnded(); ci.Next()) { + AudioChunk& c = *ci; + EXPECT_EQ(c.ChannelCount(), 2u); + for (uint32_t i = 0; i < out_frames; ++i) { + // Only pre buffered data reach output + EXPECT_FLOAT_EQ(c.ChannelData<short>()[0][i], 0.0); + EXPECT_FLOAT_EQ(c.ChannelData<short>()[1][i], 0.0); + } + } + + // Update out rate + out_rate = 44100; + dr.UpdateOutRate(out_rate); + out_frames = in_frames * out_rate / in_rate; + EXPECT_EQ(out_frames, 18u); + // Even if we provide no input if we have enough buffered input, we can create + // output + AudioSegment s1 = dr.Resample(out_frames); + EXPECT_EQ(s1.GetDuration(), out_frames); + EXPECT_EQ(s1.GetType(), MediaSegment::AUDIO); + EXPECT_TRUE(!s1.IsNull()); + EXPECT_TRUE(!s1.IsEmpty()); +} + +TEST(TestAudioResampler, OutAudioSegmentFail_Float) +{ + const uint32_t in_frames = 130; + const uint32_t out_frames = 300; + uint32_t channels = 2; + uint32_t in_rate = 24000; + uint32_t out_rate = 48000; + + uint32_t pre_buffer = 5; + + AudioResampler dr(in_rate, out_rate, pre_buffer); + AudioSegment inSegment = + CreateAudioSegment<float>(in_frames, channels, AUDIO_FORMAT_FLOAT32); + dr.AppendInput(inSegment); + + AudioSegment s = dr.Resample(out_frames); + EXPECT_EQ(s.GetDuration(), 0); + EXPECT_EQ(s.GetType(), MediaSegment::AUDIO); + EXPECT_TRUE(s.IsNull()); + EXPECT_TRUE(s.IsEmpty()); +} + +TEST(TestAudioResampler, InAudioSegment_Float) +{ + uint32_t in_frames = 10; + uint32_t out_frames = 40; + uint32_t channels = 2; + uint32_t in_rate = 24000; + uint32_t out_rate = 48000; + + uint32_t pre_buffer = 10; + AudioResampler dr(in_rate, out_rate, pre_buffer); + + AudioSegment inSegment; + + AudioChunk chunk1; + chunk1.SetNull(in_frames / 2); + inSegment.AppendAndConsumeChunk(&chunk1); + + AudioChunk chunk2; + nsTArray<nsTArray<float>> buffer; + buffer.AppendElements(channels); + + nsTArray<const float*> bufferPtrs; + bufferPtrs.AppendElements(channels); + + for (uint32_t i = 0; i < channels; ++i) { + float* ptr = buffer[i].AppendElements(5); + bufferPtrs[i] = ptr; + for (uint32_t j = 0; j < 5; ++j) { + ptr[j] = 0.01f * j; + } + } + + chunk2.mBuffer = new mozilla::SharedChannelArrayBuffer(std::move(buffer)); + chunk2.mBufferFormat = AUDIO_FORMAT_FLOAT32; + chunk2.mChannelData.AppendElements(channels); + for (uint32_t i = 0; i < channels; ++i) { + chunk2.mChannelData[i] = bufferPtrs[i]; + } + chunk2.mDuration = in_frames / 2; + inSegment.AppendAndConsumeChunk(&chunk2); + + dr.AppendInput(inSegment); + AudioSegment outSegment = dr.Resample(out_frames); + // Faild because the first chunk is ignored + EXPECT_EQ(outSegment.GetDuration(), 0u); + EXPECT_EQ(outSegment.MaxChannelCount(), 0u); + + // Add the 5 more frames that are missing + dr.AppendInput(inSegment); + AudioSegment outSegment2 = dr.Resample(out_frames); + EXPECT_EQ(outSegment2.GetDuration(), 40u); + EXPECT_EQ(outSegment2.MaxChannelCount(), 2u); +} + +TEST(TestAudioResampler, InAudioSegment_Short) +{ + uint32_t in_frames = 10; + uint32_t out_frames = 40; + uint32_t channels = 2; + uint32_t in_rate = 24000; + uint32_t out_rate = 48000; + + uint32_t pre_buffer = 10; + AudioResampler dr(in_rate, out_rate, pre_buffer); + + AudioSegment inSegment; + + // The null chunk at the beginning will be ignored. + AudioChunk chunk1; + chunk1.SetNull(in_frames / 2); + inSegment.AppendAndConsumeChunk(&chunk1); + + AudioChunk chunk2; + nsTArray<nsTArray<short>> buffer; + buffer.AppendElements(channels); + + nsTArray<const short*> bufferPtrs; + bufferPtrs.AppendElements(channels); + + for (uint32_t i = 0; i < channels; ++i) { + short* ptr = buffer[i].AppendElements(5); + bufferPtrs[i] = ptr; + for (uint32_t j = 0; j < 5; ++j) { + ptr[j] = j; + } + } + + chunk2.mBuffer = new mozilla::SharedChannelArrayBuffer(std::move(buffer)); + chunk2.mBufferFormat = AUDIO_FORMAT_S16; + chunk2.mChannelData.AppendElements(channels); + for (uint32_t i = 0; i < channels; ++i) { + chunk2.mChannelData[i] = bufferPtrs[i]; + } + chunk2.mDuration = in_frames / 2; + inSegment.AppendAndConsumeChunk(&chunk2); + + dr.AppendInput(inSegment); + AudioSegment outSegment = dr.Resample(out_frames); + // Faild because the first chunk is ignored + EXPECT_EQ(outSegment.GetDuration(), 0u); + EXPECT_EQ(outSegment.MaxChannelCount(), 0u); + + dr.AppendInput(inSegment); + AudioSegment outSegment2 = dr.Resample(out_frames); + EXPECT_EQ(outSegment2.GetDuration(), 40u); + EXPECT_EQ(outSegment2.MaxChannelCount(), 2u); +} + +TEST(TestAudioResampler, ChannelChange_MonoToStereo) +{ + uint32_t in_frames = 10; + uint32_t out_frames = 40; + // uint32_t channels = 2; + uint32_t in_rate = 24000; + uint32_t out_rate = 48000; + + uint32_t pre_buffer = 0; + + AudioResampler dr(in_rate, out_rate, pre_buffer); + + AudioChunk monoChunk = + CreateAudioChunk<float>(in_frames, 1, AUDIO_FORMAT_FLOAT32); + AudioChunk stereoChunk = + CreateAudioChunk<float>(in_frames, 2, AUDIO_FORMAT_FLOAT32); + + AudioSegment inSegment; + inSegment.AppendAndConsumeChunk(&monoChunk); + inSegment.AppendAndConsumeChunk(&stereoChunk); + dr.AppendInput(inSegment); + + AudioSegment s = dr.Resample(out_frames); + EXPECT_EQ(s.GetDuration(), 40); + EXPECT_EQ(s.GetType(), MediaSegment::AUDIO); + EXPECT_TRUE(!s.IsNull()); + EXPECT_TRUE(!s.IsEmpty()); + EXPECT_EQ(s.MaxChannelCount(), 2u); +} + +TEST(TestAudioResampler, ChannelChange_StereoToMono) +{ + uint32_t in_frames = 10; + uint32_t out_frames = 40; + // uint32_t channels = 2; + uint32_t in_rate = 24000; + uint32_t out_rate = 48000; + + uint32_t pre_buffer = 0; + + AudioResampler dr(in_rate, out_rate, pre_buffer); + + AudioChunk monoChunk = + CreateAudioChunk<float>(in_frames, 1, AUDIO_FORMAT_FLOAT32); + AudioChunk stereoChunk = + CreateAudioChunk<float>(in_frames, 2, AUDIO_FORMAT_FLOAT32); + + AudioSegment inSegment; + inSegment.AppendAndConsumeChunk(&stereoChunk); + inSegment.AppendAndConsumeChunk(&monoChunk); + dr.AppendInput(inSegment); + + AudioSegment s = dr.Resample(out_frames); + EXPECT_EQ(s.GetDuration(), 40); + EXPECT_EQ(s.GetType(), MediaSegment::AUDIO); + EXPECT_TRUE(!s.IsNull()); + EXPECT_TRUE(!s.IsEmpty()); + EXPECT_EQ(s.MaxChannelCount(), 1u); +} + +TEST(TestAudioResampler, ChannelChange_StereoToQuad) +{ + uint32_t in_frames = 10; + uint32_t out_frames = 40; + // uint32_t channels = 2; + uint32_t in_rate = 24000; + uint32_t out_rate = 48000; + + uint32_t pre_buffer = 0; + + AudioResampler dr(in_rate, out_rate, pre_buffer); + + AudioChunk stereoChunk = + CreateAudioChunk<float>(in_frames, 2, AUDIO_FORMAT_FLOAT32); + AudioChunk quadChunk = + CreateAudioChunk<float>(in_frames, 4, AUDIO_FORMAT_FLOAT32); + + AudioSegment inSegment; + inSegment.AppendAndConsumeChunk(&stereoChunk); + inSegment.AppendAndConsumeChunk(&quadChunk); + dr.AppendInput(inSegment); + + AudioSegment s = dr.Resample(out_frames); + EXPECT_EQ(s.GetDuration(), 0); + EXPECT_EQ(s.GetType(), MediaSegment::AUDIO); + EXPECT_TRUE(s.IsNull()); + EXPECT_TRUE(s.IsEmpty()); + + AudioSegment s2 = dr.Resample(out_frames / 2); + EXPECT_EQ(s2.GetDuration(), out_frames / 2); + EXPECT_EQ(s2.GetType(), MediaSegment::AUDIO); + EXPECT_TRUE(!s2.IsNull()); + EXPECT_TRUE(!s2.IsEmpty()); +} + +TEST(TestAudioResampler, ChannelChange_QuadToStereo) +{ + uint32_t in_frames = 10; + uint32_t out_frames = 40; + // uint32_t channels = 2; + uint32_t in_rate = 24000; + uint32_t out_rate = 48000; + + AudioResampler dr(in_rate, out_rate); + + AudioChunk stereoChunk = + CreateAudioChunk<float>(in_frames, 2, AUDIO_FORMAT_FLOAT32); + AudioChunk quadChunk = + CreateAudioChunk<float>(in_frames, 4, AUDIO_FORMAT_FLOAT32); + + AudioSegment inSegment; + inSegment.AppendAndConsumeChunk(&quadChunk); + inSegment.AppendAndConsumeChunk(&stereoChunk); + dr.AppendInput(inSegment); + + AudioSegment s = dr.Resample(out_frames); + EXPECT_EQ(s.GetDuration(), 0); + EXPECT_EQ(s.GetType(), MediaSegment::AUDIO); + EXPECT_TRUE(s.IsNull()); + EXPECT_TRUE(s.IsEmpty()); + + AudioSegment s2 = dr.Resample(out_frames / 2); + EXPECT_EQ(s2.GetDuration(), out_frames / 2); + EXPECT_EQ(s2.GetType(), MediaSegment::AUDIO); + EXPECT_TRUE(!s2.IsNull()); + EXPECT_TRUE(!s2.IsEmpty()); +} + +void printAudioSegment(const AudioSegment& segment); + +TEST(TestAudioResampler, ChannelChange_Discontinuity) +{ + uint32_t in_rate = 24000; + uint32_t out_rate = 48000; + + const float amplitude = 0.5; + const float frequency = 200; + const float phase = 0.0; + float time = 0.0; + const float deltaTime = 1.0f / static_cast<float>(in_rate); + + uint32_t in_frames = in_rate / 100; + uint32_t out_frames = out_rate / 100; + AudioResampler dr(in_rate, out_rate); + + AudioChunk monoChunk = + CreateAudioChunk<float>(in_frames, 1, AUDIO_FORMAT_FLOAT32); + for (uint32_t i = 0; i < monoChunk.GetDuration(); ++i) { + double value = amplitude * sin(2 * M_PI * frequency * time + phase); + monoChunk.ChannelDataForWrite<float>(0)[i] = static_cast<float>(value); + time += deltaTime; + } + AudioChunk stereoChunk = + CreateAudioChunk<float>(in_frames, 2, AUDIO_FORMAT_FLOAT32); + for (uint32_t i = 0; i < stereoChunk.GetDuration(); ++i) { + double value = amplitude * sin(2 * M_PI * frequency * time + phase); + stereoChunk.ChannelDataForWrite<float>(0)[i] = static_cast<float>(value); + if (stereoChunk.ChannelCount() == 2) { + stereoChunk.ChannelDataForWrite<float>(1)[i] = value; + } + time += deltaTime; + } + + AudioSegment inSegment; + inSegment.AppendAndConsumeChunk(&stereoChunk); + // printAudioSegment(inSegment); + + dr.AppendInput(inSegment); + AudioSegment s = dr.Resample(out_frames); + // printAudioSegment(s); + + AudioSegment inSegment2; + inSegment2.AppendAndConsumeChunk(&monoChunk); + // The resampler here is updated due to the channel change and that creates + // discontinuity. + dr.AppendInput(inSegment2); + AudioSegment s2 = dr.Resample(out_frames); + // printAudioSegment(s2); + + EXPECT_EQ(s2.GetDuration(), 480); + EXPECT_EQ(s2.GetType(), MediaSegment::AUDIO); + EXPECT_TRUE(!s2.IsNull()); + EXPECT_TRUE(!s2.IsEmpty()); + EXPECT_EQ(s2.MaxChannelCount(), 1u); +} + +TEST(TestAudioResampler, ChannelChange_Discontinuity2) +{ + uint32_t in_rate = 24000; + uint32_t out_rate = 48000; + + const float amplitude = 0.5; + const float frequency = 200; + const float phase = 0.0; + float time = 0.0; + const float deltaTime = 1.0f / static_cast<float>(in_rate); + + uint32_t in_frames = in_rate / 100; + uint32_t out_frames = out_rate / 100; + AudioResampler dr(in_rate, out_rate, 10); + + AudioChunk monoChunk = + CreateAudioChunk<float>(in_frames / 2, 1, AUDIO_FORMAT_FLOAT32); + for (uint32_t i = 0; i < monoChunk.GetDuration(); ++i) { + double value = amplitude * sin(2 * M_PI * frequency * time + phase); + monoChunk.ChannelDataForWrite<float>(0)[i] = static_cast<float>(value); + time += deltaTime; + } + AudioChunk stereoChunk = + CreateAudioChunk<float>(in_frames / 2, 2, AUDIO_FORMAT_FLOAT32); + for (uint32_t i = 0; i < stereoChunk.GetDuration(); ++i) { + double value = amplitude * sin(2 * M_PI * frequency * time + phase); + stereoChunk.ChannelDataForWrite<float>(0)[i] = static_cast<float>(value); + if (stereoChunk.ChannelCount() == 2) { + stereoChunk.ChannelDataForWrite<float>(1)[i] = value; + } + time += deltaTime; + } + + AudioSegment inSegment; + inSegment.AppendAndConsumeChunk(&monoChunk); + inSegment.AppendAndConsumeChunk(&stereoChunk); + // printAudioSegment(inSegment); + + dr.AppendInput(inSegment); + AudioSegment s1 = dr.Resample(out_frames); + // printAudioSegment(s1); + + EXPECT_EQ(s1.GetDuration(), 480); + EXPECT_EQ(s1.GetType(), MediaSegment::AUDIO); + EXPECT_TRUE(!s1.IsNull()); + EXPECT_TRUE(!s1.IsEmpty()); + EXPECT_EQ(s1.MaxChannelCount(), 2u); + + // The resampler here is updated due to the channel change and that creates + // discontinuity. + dr.AppendInput(inSegment); + AudioSegment s2 = dr.Resample(out_frames); + // printAudioSegment(s2); + + EXPECT_EQ(s2.GetDuration(), 480); + EXPECT_EQ(s2.GetType(), MediaSegment::AUDIO); + EXPECT_TRUE(!s2.IsNull()); + EXPECT_TRUE(!s2.IsEmpty()); + EXPECT_EQ(s2.MaxChannelCount(), 2u); +} + +TEST(TestAudioResampler, ChannelChange_Discontinuity3) +{ + uint32_t in_rate = 48000; + uint32_t out_rate = 48000; + + const float amplitude = 0.5; + const float frequency = 200; + const float phase = 0.0; + float time = 0.0; + const float deltaTime = 1.0f / static_cast<float>(in_rate); + + uint32_t in_frames = in_rate / 100; + uint32_t out_frames = out_rate / 100; + AudioResampler dr(in_rate, out_rate, 10); + + AudioChunk stereoChunk = + CreateAudioChunk<float>(in_frames, 2, AUDIO_FORMAT_FLOAT32); + for (uint32_t i = 0; i < stereoChunk.GetDuration(); ++i) { + double value = amplitude * sin(2 * M_PI * frequency * time + phase); + stereoChunk.ChannelDataForWrite<float>(0)[i] = static_cast<float>(value); + if (stereoChunk.ChannelCount() == 2) { + stereoChunk.ChannelDataForWrite<float>(1)[i] = value; + } + time += deltaTime; + } + + AudioSegment inSegment; + inSegment.AppendAndConsumeChunk(&stereoChunk); + // printAudioSegment(inSegment); + + dr.AppendInput(inSegment); + AudioSegment s = dr.Resample(out_frames); + // printAudioSegment(s); + + // The resampler here is updated due to the rate change. This is because the + // in and out rate was the same so a pass through logice was used. By updating + // the out rate to something different than the in rate, the resampler will + // start being use dand discontinuity will exist. + dr.UpdateOutRate(out_rate + 100); + dr.AppendInput(inSegment); + AudioSegment s2 = dr.Resample(out_frames); + // printAudioSegment(s2); + + EXPECT_EQ(s2.GetDuration(), 480); + EXPECT_EQ(s2.GetType(), MediaSegment::AUDIO); + EXPECT_TRUE(!s2.IsNull()); + EXPECT_TRUE(!s2.IsEmpty()); + EXPECT_EQ(s2.MaxChannelCount(), 2u); +} diff --git a/dom/media/gtest/TestGMPCrossOrigin.cpp b/dom/media/gtest/TestGMPCrossOrigin.cpp new file mode 100644 index 0000000000..052e59d4cb --- /dev/null +++ b/dom/media/gtest/TestGMPCrossOrigin.cpp @@ -0,0 +1,208 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "mozilla/StaticPtr.h" +#include "GMPTestMonitor.h" +#include "GMPVideoDecoderProxy.h" +#include "GMPVideoEncoderProxy.h" +#include "GMPServiceParent.h" +#include "nsAppDirectoryServiceDefs.h" +#include "mozilla/Atomics.h" +#include "mozilla/DebugOnly.h" +#include "nsThreadUtils.h" + +using namespace mozilla; +using namespace mozilla::gmp; + +struct GMPTestRunner { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPTestRunner) + + GMPTestRunner() = default; + void DoTest(void (GMPTestRunner::*aTestMethod)(GMPTestMonitor&)); + void RunTestGMPTestCodec1(GMPTestMonitor& aMonitor); + void RunTestGMPTestCodec2(GMPTestMonitor& aMonitor); + void RunTestGMPTestCodec3(GMPTestMonitor& aMonitor); + void RunTestGMPCrossOrigin1(GMPTestMonitor& aMonitor); + void RunTestGMPCrossOrigin2(GMPTestMonitor& aMonitor); + void RunTestGMPCrossOrigin3(GMPTestMonitor& aMonitor); + void RunTestGMPCrossOrigin4(GMPTestMonitor& aMonitor); + + private: + ~GMPTestRunner() = default; +}; + +template <class T, class Base, + nsresult (NS_STDCALL GeckoMediaPluginService::*Getter)( + GMPCrashHelper*, nsTArray<nsCString>*, const nsACString&, + UniquePtr<Base>&&)> +class RunTestGMPVideoCodec : public Base { + public: + void Done(T* aGMP, GMPVideoHost* aHost) override { + EXPECT_TRUE(aGMP); + EXPECT_TRUE(aHost); + if (aGMP) { + aGMP->Close(); + } + mMonitor.SetFinished(); + } + + static void Run(GMPTestMonitor& aMonitor, const nsCString& aOrigin) { + UniquePtr<GMPCallbackType> callback(new RunTestGMPVideoCodec(aMonitor)); + Get(aOrigin, std::move(callback)); + } + + protected: + typedef T GMPCodecType; + typedef Base GMPCallbackType; + + explicit RunTestGMPVideoCodec(GMPTestMonitor& aMonitor) + : mMonitor(aMonitor) {} + + static nsresult Get(const nsACString& aNodeId, UniquePtr<Base>&& aCallback) { + nsTArray<nsCString> tags; + tags.AppendElement("h264"_ns); + tags.AppendElement("fake"_ns); + + RefPtr<GeckoMediaPluginService> service = + GeckoMediaPluginService::GetGeckoMediaPluginService(); + return ((*service).*Getter)(nullptr, &tags, aNodeId, std::move(aCallback)); + } + + GMPTestMonitor& mMonitor; +}; + +typedef RunTestGMPVideoCodec<GMPVideoDecoderProxy, GetGMPVideoDecoderCallback, + &GeckoMediaPluginService::GetGMPVideoDecoder> + RunTestGMPVideoDecoder; +typedef RunTestGMPVideoCodec<GMPVideoEncoderProxy, GetGMPVideoEncoderCallback, + &GeckoMediaPluginService::GetGMPVideoEncoder> + RunTestGMPVideoEncoder; + +void GMPTestRunner::RunTestGMPTestCodec1(GMPTestMonitor& aMonitor) { + RunTestGMPVideoDecoder::Run(aMonitor, "o"_ns); +} + +void GMPTestRunner::RunTestGMPTestCodec2(GMPTestMonitor& aMonitor) { + RunTestGMPVideoDecoder::Run(aMonitor, ""_ns); +} + +void GMPTestRunner::RunTestGMPTestCodec3(GMPTestMonitor& aMonitor) { + RunTestGMPVideoEncoder::Run(aMonitor, ""_ns); +} + +template <class Base> +class RunTestGMPCrossOrigin : public Base { + public: + void Done(typename Base::GMPCodecType* aGMP, GMPVideoHost* aHost) override { + EXPECT_TRUE(aGMP); + + UniquePtr<typename Base::GMPCallbackType> callback( + new Step2(Base::mMonitor, aGMP, mShouldBeEqual)); + nsresult rv = Base::Get(mOrigin2, std::move(callback)); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + if (NS_FAILED(rv)) { + Base::mMonitor.SetFinished(); + } + } + + static void Run(GMPTestMonitor& aMonitor, const nsCString& aOrigin1, + const nsCString& aOrigin2) { + UniquePtr<typename Base::GMPCallbackType> callback( + new RunTestGMPCrossOrigin<Base>(aMonitor, aOrigin1, aOrigin2)); + nsresult rv = Base::Get(aOrigin1, std::move(callback)); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + if (NS_FAILED(rv)) { + aMonitor.SetFinished(); + } + } + + private: + RunTestGMPCrossOrigin(GMPTestMonitor& aMonitor, const nsCString& aOrigin1, + const nsCString& aOrigin2) + : Base(aMonitor), + mGMP(nullptr), + mOrigin2(aOrigin2), + mShouldBeEqual(aOrigin1.Equals(aOrigin2)) {} + + class Step2 : public Base { + public: + Step2(GMPTestMonitor& aMonitor, typename Base::GMPCodecType* aGMP, + bool aShouldBeEqual) + : Base(aMonitor), mGMP(aGMP), mShouldBeEqual(aShouldBeEqual) {} + void Done(typename Base::GMPCodecType* aGMP, GMPVideoHost* aHost) override { + EXPECT_TRUE(aGMP); + if (aGMP) { + EXPECT_TRUE(mGMP && (mGMP->GetPluginId() == aGMP->GetPluginId()) == + mShouldBeEqual); + } + if (mGMP) { + mGMP->Close(); + } + Base::Done(aGMP, aHost); + } + + private: + typename Base::GMPCodecType* mGMP; + bool mShouldBeEqual; + }; + + typename Base::GMPCodecType* mGMP; + nsCString mOrigin2; + bool mShouldBeEqual; +}; + +typedef RunTestGMPCrossOrigin<RunTestGMPVideoDecoder> + RunTestGMPVideoDecoderCrossOrigin; +typedef RunTestGMPCrossOrigin<RunTestGMPVideoEncoder> + RunTestGMPVideoEncoderCrossOrigin; + +void GMPTestRunner::RunTestGMPCrossOrigin1(GMPTestMonitor& aMonitor) { + RunTestGMPVideoDecoderCrossOrigin::Run(aMonitor, "origin1"_ns, "origin2"_ns); +} + +void GMPTestRunner::RunTestGMPCrossOrigin2(GMPTestMonitor& aMonitor) { + RunTestGMPVideoEncoderCrossOrigin::Run(aMonitor, "origin1"_ns, "origin2"_ns); +} + +void GMPTestRunner::RunTestGMPCrossOrigin3(GMPTestMonitor& aMonitor) { + RunTestGMPVideoDecoderCrossOrigin::Run(aMonitor, "origin1"_ns, "origin1"_ns); +} + +void GMPTestRunner::RunTestGMPCrossOrigin4(GMPTestMonitor& aMonitor) { + RunTestGMPVideoEncoderCrossOrigin::Run(aMonitor, "origin1"_ns, "origin1"_ns); +} + +void GMPTestRunner::DoTest( + void (GMPTestRunner::*aTestMethod)(GMPTestMonitor&)) { + RefPtr<GeckoMediaPluginService> service = + GeckoMediaPluginService::GetGeckoMediaPluginService(); + nsCOMPtr<nsIThread> thread; + EXPECT_TRUE(NS_SUCCEEDED(service->GetThread(getter_AddRefs(thread)))); + + GMPTestMonitor monitor; + thread->Dispatch(NewRunnableMethod<GMPTestMonitor&>( + "GMPTestRunner::DoTest", this, aTestMethod, monitor), + NS_DISPATCH_NORMAL); + monitor.AwaitFinished(); +} + +TEST(GeckoMediaPlugins, GMPTestCodec) +{ + RefPtr<GMPTestRunner> runner = new GMPTestRunner(); + runner->DoTest(&GMPTestRunner::RunTestGMPTestCodec1); + runner->DoTest(&GMPTestRunner::RunTestGMPTestCodec2); + runner->DoTest(&GMPTestRunner::RunTestGMPTestCodec3); +} + +TEST(GeckoMediaPlugins, GMPCrossOrigin) +{ + RefPtr<GMPTestRunner> runner = new GMPTestRunner(); + runner->DoTest(&GMPTestRunner::RunTestGMPCrossOrigin1); + runner->DoTest(&GMPTestRunner::RunTestGMPCrossOrigin2); + runner->DoTest(&GMPTestRunner::RunTestGMPCrossOrigin3); + runner->DoTest(&GMPTestRunner::RunTestGMPCrossOrigin4); +} diff --git a/dom/media/gtest/TestGMPRemoveAndDelete.cpp b/dom/media/gtest/TestGMPRemoveAndDelete.cpp new file mode 100644 index 0000000000..5c5371e0a3 --- /dev/null +++ b/dom/media/gtest/TestGMPRemoveAndDelete.cpp @@ -0,0 +1,469 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "GMPService.h" +#include "GMPServiceParent.h" +#include "GMPTestMonitor.h" +#include "GMPUtils.h" +#include "GMPVideoDecoderProxy.h" +#include "gmp-api/gmp-video-host.h" +#include "gtest/gtest.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPtr.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIObserverService.h" + +#define GMP_DIR_NAME u"gmp-fakeopenh264"_ns +#define GMP_OLD_VERSION u"1.0"_ns +#define GMP_NEW_VERSION u"1.1"_ns + +#define GMP_DELETED_TOPIC "gmp-directory-deleted" + +#define EXPECT_OK(X) EXPECT_TRUE(NS_SUCCEEDED(X)) + +using namespace mozilla; +using namespace mozilla::gmp; + +class GMPRemoveTest : public nsIObserver, public GMPVideoDecoderCallbackProxy { + public: + GMPRemoveTest(); + + NS_DECL_THREADSAFE_ISUPPORTS + + // Called when a GMP plugin directory has been successfully deleted. + // |aData| will contain the directory path. + NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) override; + + // Create a new GMP plugin directory that we can trash and add it to the GMP + // service. Remove the original plugin directory. Original plugin directory + // gets re-added at destruction. + void Setup(); + + bool CreateVideoDecoder(nsCString aNodeId = ""_ns); + void CloseVideoDecoder(); + + void DeletePluginDirectory(bool aCanDefer); + + // Decode a dummy frame. + GMPErr Decode(); + + // Wait until TestMonitor has been signaled. + void Wait(); + + // Did we get a Terminated() callback from the plugin? + bool IsTerminated(); + + // From GMPVideoDecoderCallbackProxy + // Set mDecodeResult; unblock TestMonitor. + virtual void Decoded(GMPVideoi420Frame* aDecodedFrame) override; + virtual void Error(GMPErr aError) override; + + // From GMPVideoDecoderCallbackProxy + // We expect this to be called when a plugin has been forcibly closed. + virtual void Terminated() override; + + // Ignored GMPVideoDecoderCallbackProxy members + virtual void ReceivedDecodedReferenceFrame( + const uint64_t aPictureId) override {} + virtual void ReceivedDecodedFrame(const uint64_t aPictureId) override {} + virtual void InputDataExhausted() override {} + virtual void DrainComplete() override {} + virtual void ResetComplete() override {} + + private: + virtual ~GMPRemoveTest(); + + void gmp_Decode(); + void gmp_GetVideoDecoder(nsCString aNodeId, + GMPVideoDecoderProxy** aOutDecoder, + GMPVideoHost** aOutHost); + void GeneratePlugin(); + + GMPTestMonitor mTestMonitor; + nsCOMPtr<nsIThread> mGMPThread; + + bool mIsTerminated; + + // Path to the cloned GMP we have created. + nsString mTmpPath; + nsCOMPtr<nsIFile> mTmpDir; + + // Path to the original GMP. Store so that we can re-add it after we're done + // testing. + nsString mOriginalPath; + + GMPVideoDecoderProxy* mDecoder; + GMPVideoHost* mHost; + GMPErr mDecodeResult; +}; + +/* + * Simple test that the plugin is deleted when forcibly removed and deleted. + */ +TEST(GeckoMediaPlugins, RemoveAndDeleteForcedSimple) +{ + RefPtr<GMPRemoveTest> test(new GMPRemoveTest()); + + test->Setup(); + test->DeletePluginDirectory(false /* force immediate */); + test->Wait(); +} + +/* + * Simple test that the plugin is deleted when deferred deletion is allowed. + */ +TEST(GeckoMediaPlugins, RemoveAndDeleteDeferredSimple) +{ + RefPtr<GMPRemoveTest> test(new GMPRemoveTest()); + + test->Setup(); + test->DeletePluginDirectory(true /* can defer */); + test->Wait(); +} + +/* + * Test that the plugin is unavailable immediately after a forced + * RemoveAndDelete, and that the plugin is deleted afterwards. + */ +TEST(GeckoMediaPlugins, RemoveAndDeleteForcedInUse) +{ + RefPtr<GMPRemoveTest> test(new GMPRemoveTest()); + + test->Setup(); + EXPECT_TRUE(test->CreateVideoDecoder("thisOrigin"_ns)); + + // Test that we can decode a frame. + GMPErr err = test->Decode(); + EXPECT_EQ(err, GMPNoErr); + + test->DeletePluginDirectory(false /* force immediate */); + test->Wait(); + + // Test that the VideoDecoder is no longer available. + EXPECT_FALSE(test->CreateVideoDecoder("thisOrigin"_ns)); + + // Test that we were notified of the plugin's destruction. + EXPECT_TRUE(test->IsTerminated()); +} + +/* + * Test that the plugin is still usable after a deferred RemoveAndDelete, and + * that the plugin is deleted afterwards. + */ +TEST(GeckoMediaPlugins, RemoveAndDeleteDeferredInUse) +{ + RefPtr<GMPRemoveTest> test(new GMPRemoveTest()); + + test->Setup(); + EXPECT_TRUE(test->CreateVideoDecoder("thisOrigin"_ns)); + + // Make sure decoding works before we do anything. + GMPErr err = test->Decode(); + EXPECT_EQ(err, GMPNoErr); + + test->DeletePluginDirectory(true /* can defer */); + + // Test that decoding still works. + err = test->Decode(); + EXPECT_EQ(err, GMPNoErr); + + // Test that this origin is still able to fetch the video decoder. + EXPECT_TRUE(test->CreateVideoDecoder("thisOrigin"_ns)); + + test->CloseVideoDecoder(); + test->Wait(); +} + +static StaticRefPtr<GeckoMediaPluginService> gService; +static StaticRefPtr<GeckoMediaPluginServiceParent> gServiceParent; + +static GeckoMediaPluginService* GetService() { + if (!gService) { + RefPtr<GeckoMediaPluginService> service = + GeckoMediaPluginService::GetGeckoMediaPluginService(); + gService = service; + } + + return gService.get(); +} + +static GeckoMediaPluginServiceParent* GetServiceParent() { + if (!gServiceParent) { + RefPtr<GeckoMediaPluginServiceParent> parent = + GeckoMediaPluginServiceParent::GetSingleton(); + gServiceParent = parent; + } + + return gServiceParent.get(); +} + +NS_IMPL_ISUPPORTS(GMPRemoveTest, nsIObserver) + +GMPRemoveTest::GMPRemoveTest() + : mIsTerminated(false), mDecoder(nullptr), mHost(nullptr) {} + +GMPRemoveTest::~GMPRemoveTest() { + bool exists; + EXPECT_TRUE(NS_SUCCEEDED(mTmpDir->Exists(&exists)) && !exists); + + EXPECT_OK(GetServiceParent()->AddPluginDirectory(mOriginalPath)); +} + +void GMPRemoveTest::Setup() { + GeneratePlugin(); + GetService()->GetThread(getter_AddRefs(mGMPThread)); + + // Spin the event loop until the GMP service has had a chance to complete + // adding GMPs from MOZ_GMP_PATH. Otherwise, the RemovePluginDirectory() + // below may complete before we're finished adding GMPs from MOZ_GMP_PATH, + // and we'll end up not removing the GMP, and the test will fail. + nsCOMPtr<nsISerialEventTarget> thread(GetServiceParent()->GetGMPThread()); + EXPECT_TRUE(thread); + GMPTestMonitor* mon = &mTestMonitor; + GetServiceParent()->EnsureInitialized()->Then( + thread, __func__, [mon]() { mon->SetFinished(); }, + [mon]() { mon->SetFinished(); }); + mTestMonitor.AwaitFinished(); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + obs->AddObserver(this, GMP_DELETED_TOPIC, false /* strong ref */); + EXPECT_OK(GetServiceParent()->RemovePluginDirectory(mOriginalPath)); + + GetServiceParent()->AsyncAddPluginDirectory(mTmpPath)->Then( + thread, __func__, [mon]() { mon->SetFinished(); }, + [mon]() { mon->SetFinished(); }); + mTestMonitor.AwaitFinished(); +} + +bool GMPRemoveTest::CreateVideoDecoder(nsCString aNodeId) { + GMPVideoHost* host; + GMPVideoDecoderProxy* decoder = nullptr; + + mGMPThread->Dispatch( + NewNonOwningRunnableMethod<nsCString, GMPVideoDecoderProxy**, + GMPVideoHost**>( + "GMPRemoveTest::gmp_GetVideoDecoder", this, + &GMPRemoveTest::gmp_GetVideoDecoder, aNodeId, &decoder, &host), + NS_DISPATCH_NORMAL); + + mTestMonitor.AwaitFinished(); + + if (!decoder) { + return false; + } + + GMPVideoCodec codec; + memset(&codec, 0, sizeof(codec)); + codec.mGMPApiVersion = 33; + + nsTArray<uint8_t> empty; + mGMPThread->Dispatch( + NewNonOwningRunnableMethod<const GMPVideoCodec&, const nsTArray<uint8_t>&, + GMPVideoDecoderCallbackProxy*, int32_t>( + "GMPVideoDecoderProxy::InitDecode", decoder, + &GMPVideoDecoderProxy::InitDecode, codec, empty, this, + 1 /* core count */), + NS_DISPATCH_SYNC); + + if (mDecoder) { + CloseVideoDecoder(); + } + + mDecoder = decoder; + mHost = host; + + return true; +} + +void GMPRemoveTest::gmp_GetVideoDecoder(nsCString aNodeId, + GMPVideoDecoderProxy** aOutDecoder, + GMPVideoHost** aOutHost) { + nsTArray<nsCString> tags; + tags.AppendElement("h264"_ns); + tags.AppendElement("fake"_ns); + + class Callback : public GetGMPVideoDecoderCallback { + public: + Callback(GMPTestMonitor* aMonitor, GMPVideoDecoderProxy** aDecoder, + GMPVideoHost** aHost) + : mMonitor(aMonitor), mDecoder(aDecoder), mHost(aHost) {} + virtual void Done(GMPVideoDecoderProxy* aDecoder, + GMPVideoHost* aHost) override { + *mDecoder = aDecoder; + *mHost = aHost; + mMonitor->SetFinished(); + } + + private: + GMPTestMonitor* mMonitor; + GMPVideoDecoderProxy** mDecoder; + GMPVideoHost** mHost; + }; + + UniquePtr<GetGMPVideoDecoderCallback> cb( + new Callback(&mTestMonitor, aOutDecoder, aOutHost)); + + if (NS_FAILED(GetService()->GetGMPVideoDecoder(nullptr, &tags, aNodeId, + std::move(cb)))) { + mTestMonitor.SetFinished(); + } +} + +void GMPRemoveTest::CloseVideoDecoder() { + mGMPThread->Dispatch( + NewNonOwningRunnableMethod("GMPVideoDecoderProxy::Close", mDecoder, + &GMPVideoDecoderProxy::Close), + NS_DISPATCH_SYNC); + + mDecoder = nullptr; + mHost = nullptr; +} + +void GMPRemoveTest::DeletePluginDirectory(bool aCanDefer) { + GetServiceParent()->RemoveAndDeletePluginDirectory(mTmpPath, aCanDefer); +} + +GMPErr GMPRemoveTest::Decode() { + mGMPThread->Dispatch( + NewNonOwningRunnableMethod("GMPRemoveTest::gmp_Decode", this, + &GMPRemoveTest::gmp_Decode), + NS_DISPATCH_NORMAL); + + mTestMonitor.AwaitFinished(); + return mDecodeResult; +} + +void GMPRemoveTest::gmp_Decode() { +// from gmp-fake.cpp +#pragma pack(push, 1) + struct EncodedFrame { + struct SPSNalu { + uint32_t size_; + uint8_t payload[14]; + } sps_nalu; + struct PPSNalu { + uint32_t size_; + uint8_t payload[4]; + } pps_nalu; + struct IDRNalu { + uint32_t size_; + uint8_t h264_compat_; + uint32_t magic_; + uint32_t width_; + uint32_t height_; + uint8_t y_; + uint8_t u_; + uint8_t v_; + uint32_t timestamp_; + } idr_nalu; + }; +#pragma pack(pop) + + GMPVideoFrame* absFrame; + GMPErr err = mHost->CreateFrame(kGMPEncodedVideoFrame, &absFrame); + EXPECT_EQ(err, GMPNoErr); + + GMPUniquePtr<GMPVideoEncodedFrame> frame( + static_cast<GMPVideoEncodedFrame*>(absFrame)); + err = frame->CreateEmptyFrame(sizeof(EncodedFrame) /* size */); + EXPECT_EQ(err, GMPNoErr); + + EncodedFrame* frameData = reinterpret_cast<EncodedFrame*>(frame->Buffer()); + frameData->sps_nalu.size_ = sizeof(EncodedFrame::SPSNalu) - sizeof(uint32_t); + frameData->pps_nalu.size_ = sizeof(EncodedFrame::PPSNalu) - sizeof(uint32_t); + frameData->idr_nalu.size_ = sizeof(EncodedFrame::IDRNalu) - sizeof(uint32_t); + frameData->idr_nalu.h264_compat_ = 5; + frameData->idr_nalu.magic_ = 0x004000b8; + frameData->idr_nalu.width_ = frameData->idr_nalu.height_ = 16; + + nsTArray<uint8_t> empty; + nsresult rv = + mDecoder->Decode(std::move(frame), false /* aMissingFrames */, empty); + EXPECT_OK(rv); +} + +void GMPRemoveTest::Wait() { mTestMonitor.AwaitFinished(); } + +bool GMPRemoveTest::IsTerminated() { return mIsTerminated; } + +// nsIObserver +NS_IMETHODIMP +GMPRemoveTest::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + EXPECT_TRUE(!strcmp(GMP_DELETED_TOPIC, aTopic)); + + nsString data(aData); + if (mTmpPath.Equals(data)) { + mTestMonitor.SetFinished(); + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + obs->RemoveObserver(this, GMP_DELETED_TOPIC); + } + + return NS_OK; +} + +// GMPVideoDecoderCallbackProxy +void GMPRemoveTest::Decoded(GMPVideoi420Frame* aDecodedFrame) { + aDecodedFrame->Destroy(); + mDecodeResult = GMPNoErr; + mTestMonitor.SetFinished(); +} + +// GMPVideoDecoderCallbackProxy +void GMPRemoveTest::Error(GMPErr aError) { + mDecodeResult = aError; + mTestMonitor.SetFinished(); +} + +// GMPVideoDecoderCallbackProxy +void GMPRemoveTest::Terminated() { + mIsTerminated = true; + if (mDecoder) { + mDecoder->Close(); + mDecoder = nullptr; + } +} + +void GMPRemoveTest::GeneratePlugin() { + nsresult rv; + nsCOMPtr<nsIFile> gmpDir; + nsCOMPtr<nsIFile> origDir; + nsCOMPtr<nsIFile> tmpDir; + + rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(gmpDir)); + EXPECT_OK(rv); + rv = gmpDir->Append(GMP_DIR_NAME); + EXPECT_OK(rv); + + rv = gmpDir->Clone(getter_AddRefs(origDir)); + EXPECT_OK(rv); + rv = origDir->Append(GMP_OLD_VERSION); + EXPECT_OK(rv); + + rv = gmpDir->Clone(getter_AddRefs(tmpDir)); + EXPECT_OK(rv); + rv = tmpDir->Append(GMP_NEW_VERSION); + EXPECT_OK(rv); + bool exists = false; + rv = tmpDir->Exists(&exists); + EXPECT_OK(rv); + if (exists) { + rv = tmpDir->Remove(true); + EXPECT_OK(rv); + } + rv = origDir->CopyTo(gmpDir, GMP_NEW_VERSION); + EXPECT_OK(rv); + + rv = gmpDir->Clone(getter_AddRefs(tmpDir)); + EXPECT_OK(rv); + rv = tmpDir->Append(GMP_NEW_VERSION); + EXPECT_OK(rv); + + EXPECT_OK(origDir->GetPath(mOriginalPath)); + EXPECT_OK(tmpDir->GetPath(mTmpPath)); + mTmpDir = tmpDir; +} diff --git a/dom/media/gtest/TestGMPUtils.cpp b/dom/media/gtest/TestGMPUtils.cpp new file mode 100644 index 0000000000..589b47b581 --- /dev/null +++ b/dom/media/gtest/TestGMPUtils.cpp @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "GMPUtils.h" +#include "mozilla/ArrayUtils.h" +#include "nsString.h" + +#include <string> +#include <vector> + +using namespace mozilla; + +void TestSplitAt(const char* aInput, const char* aDelims, + size_t aNumExpectedTokens, const char* aExpectedTokens[]) { + nsCString input(aInput); + nsTArray<nsCString> tokens; + SplitAt(aDelims, input, tokens); + EXPECT_EQ(tokens.Length(), aNumExpectedTokens) + << "Should get expected number of tokens"; + for (size_t i = 0; i < tokens.Length(); i++) { + EXPECT_TRUE(tokens[i].EqualsASCII(aExpectedTokens[i])) + << "Tokenize fail; expected=" << aExpectedTokens[i] + << " got=" << tokens[i].BeginReading(); + } +} + +TEST(GeckoMediaPlugins, TestSplitAt) +{ + { + const char* input = "1,2,3,4"; + const char* delims = ","; + const char* tokens[] = {"1", "2", "3", "4"}; + TestSplitAt(input, delims, MOZ_ARRAY_LENGTH(tokens), tokens); + } + + { + const char* input = "a simple, comma, seperated, list"; + const char* delims = ","; + const char* tokens[] = {"a simple", " comma", " seperated", " list"}; + TestSplitAt(input, delims, MOZ_ARRAY_LENGTH(tokens), tokens); + } + + { + const char* input = // Various platform line endings... + "line1\r\n" // Windows + "line2\r" // Old MacOSX + "line3\n" // Unix + "line4"; + const char* delims = "\r\n"; + const char* tokens[] = {"line1", "line2", "line3", "line4"}; + TestSplitAt(input, delims, MOZ_ARRAY_LENGTH(tokens), tokens); + } +} + +TEST(GeckoMediaPlugins, ToHexString) +{ + struct Test { + nsTArray<uint8_t> bytes; + std::string hex; + }; + + static const Test tests[] = { + {{0x00, 0x00}, "0000"}, + {{0xff, 0xff}, "ffff"}, + {{0xff, 0x00}, "ff00"}, + {{0x00, 0xff}, "00ff"}, + {{0xf0, 0x10}, "f010"}, + {{0x05, 0x50}, "0550"}, + {{0xf}, "0f"}, + {{0x10}, "10"}, + {{}, ""}, + {{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0xcc, 0xdd, 0xee, 0xff}, + "00112233445566778899aabbccddeeff"}, + }; + + for (const Test& test : tests) { + EXPECT_STREQ(test.hex.c_str(), ToHexString(test.bytes).get()); + } +} diff --git a/dom/media/gtest/TestGroupId.cpp b/dom/media/gtest/TestGroupId.cpp new file mode 100644 index 0000000000..0b3cbfed02 --- /dev/null +++ b/dom/media/gtest/TestGroupId.cpp @@ -0,0 +1,322 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AudioDeviceInfo.h" +#include "MediaManager.h" +#include "gmock/gmock.h" +#include "gtest/gtest-printers.h" +#include "gtest/gtest.h" +#include "mozilla/Attributes.h" +#include "mozilla/UniquePtr.h" +#include "nsTArray.h" +#include "webrtc/MediaEngineSource.h" + +using ::testing::Return; +using namespace mozilla; + +void PrintTo(const nsString& aValue, ::std::ostream* aStream) { + NS_ConvertUTF16toUTF8 str(aValue); + (*aStream) << str.get(); +} +void PrintTo(const nsCString& aValue, ::std::ostream* aStream) { + (*aStream) << aValue.get(); +} + +class MockMediaEngineSource : public MediaEngineSource { + public: + MOCK_CONST_METHOD0(GetMediaSource, dom::MediaSourceEnum()); + + /* Unused overrides */ + MOCK_CONST_METHOD0(GetName, nsString()); + MOCK_CONST_METHOD0(GetUUID, nsCString()); + MOCK_CONST_METHOD0(GetGroupId, nsString()); + MOCK_CONST_METHOD1(GetSettings, void(dom::MediaTrackSettings&)); + MOCK_METHOD4(Allocate, + nsresult(const dom::MediaTrackConstraints&, + const MediaEnginePrefs&, uint64_t, const char**)); + MOCK_METHOD2(SetTrack, + void(const RefPtr<MediaTrack>&, const PrincipalHandle&)); + MOCK_METHOD0(Start, nsresult()); + MOCK_METHOD3(Reconfigure, nsresult(const dom::MediaTrackConstraints&, + const MediaEnginePrefs&, const char**)); + MOCK_METHOD0(Stop, nsresult()); + MOCK_METHOD0(Deallocate, nsresult()); +}; + +RefPtr<AudioDeviceInfo> MakeAudioDeviceInfo(const nsString aName) { + return MakeRefPtr<AudioDeviceInfo>( + nullptr, aName, u"GroupId"_ns, u"Vendor"_ns, AudioDeviceInfo::TYPE_OUTPUT, + AudioDeviceInfo::STATE_ENABLED, AudioDeviceInfo::PREF_NONE, + AudioDeviceInfo::FMT_F32LE, AudioDeviceInfo::FMT_F32LE, 2u, 44100u, + 44100u, 44100u, 0, 0); +} + +RefPtr<MediaDevice> MakeCameraDevice(const nsString& aName, + const nsString& aGroupId) { + auto v = MakeRefPtr<MockMediaEngineSource>(); + EXPECT_CALL(*v, GetMediaSource()) + .WillRepeatedly(Return(dom::MediaSourceEnum::Camera)); + + return MakeRefPtr<MediaDevice>(v, aName, u""_ns, aGroupId, u""_ns); +} + +RefPtr<MediaDevice> MakeMicDevice(const nsString& aName, + const nsString& aGroupId) { + auto a = MakeRefPtr<MockMediaEngineSource>(); + EXPECT_CALL(*a, GetMediaSource()) + .WillRepeatedly(Return(dom::MediaSourceEnum::Microphone)); + + return MakeRefPtr<MediaDevice>(a, aName, u""_ns, aGroupId, u""_ns); +} + +RefPtr<MediaDevice> MakeSpeakerDevice(const nsString& aName, + const nsString& aGroupId) { + return MakeRefPtr<MediaDevice>(MakeAudioDeviceInfo(aName), u"ID"_ns, aGroupId, + u"RawID"_ns); +} + +/* Verify that when an audio input device name contains the video input device + * name the video device group id is updated to become equal to the audio + * device group id. */ +TEST(TestGroupId, MatchInput_PartOfName) +{ + MediaManager::MediaDeviceSet devices; + MediaManager::MediaDeviceSet audios; + + devices.AppendElement( + MakeCameraDevice(u"Vendor Model"_ns, u"Cam-Model-GroupId"_ns)); + + auto mic = + MakeMicDevice(u"Vendor Model Analog Stereo"_ns, u"Mic-Model-GroupId"_ns); + devices.AppendElement(mic); + audios.AppendElement(mic); + + MediaManager::GuessVideoDeviceGroupIDs(devices, audios); + + EXPECT_EQ(devices[0]->mGroupID, devices[1]->mGroupID) + << "Video group id is the same as audio input group id."; +} + +/* Verify that when an audio input device name is the same as the video input + * device name the video device group id is updated to become equal to the audio + * device group id. */ +TEST(TestGroupId, MatchInput_FullName) +{ + MediaManager::MediaDeviceSet devices; + MediaManager::MediaDeviceSet audios; + + devices.AppendElement( + MakeCameraDevice(u"Vendor Model"_ns, u"Cam-Model-GroupId"_ns)); + + auto mic = MakeMicDevice(u"Vendor Model"_ns, u"Mic-Model-GroupId"_ns); + devices.AppendElement(mic); + audios.AppendElement(mic); + + MediaManager::GuessVideoDeviceGroupIDs(devices, audios); + + EXPECT_EQ(devices[0]->mGroupID, devices[1]->mGroupID) + << "Video group id is the same as audio input group id."; +} + +/* Verify that when an audio input device name does not contain the video input + * device name the video device group id does not change. */ +TEST(TestGroupId, NoMatchInput) +{ + MediaManager::MediaDeviceSet devices; + MediaManager::MediaDeviceSet audios; + + nsString Cam_Model_GroupId = u"Cam-Model-GroupId"_ns; + devices.AppendElement( + MakeCameraDevice(u"Vendor Model"_ns, Cam_Model_GroupId)); + + audios.AppendElement( + MakeMicDevice(u"Model Analog Stereo"_ns, u"Mic-Model-GroupId"_ns)); + + MediaManager::GuessVideoDeviceGroupIDs(devices, audios); + + EXPECT_EQ(devices[0]->mGroupID, Cam_Model_GroupId) + << "Video group id has not been updated."; + EXPECT_NE(devices[0]->mGroupID, audios[0]->mGroupID) + << "Video group id is different than audio input group id."; +} + +/* Verify that when more that one audio input and more than one audio output + * device name contain the video input device name the video device group id + * does not change. */ +TEST(TestGroupId, NoMatch_TwoIdenticalDevices) +{ + MediaManager::MediaDeviceSet devices; + MediaManager::MediaDeviceSet audios; + + nsString Cam_Model_GroupId = u"Cam-Model-GroupId"_ns; + devices.AppendElement( + MakeCameraDevice(u"Vendor Model"_ns, Cam_Model_GroupId)); + + audios.AppendElement( + MakeMicDevice(u"Vendor Model Analog Stereo"_ns, u"Mic-Model-GroupId"_ns)); + audios.AppendElement( + MakeMicDevice(u"Vendor Model Analog Stereo"_ns, u"Mic-Model-GroupId"_ns)); + + audios.AppendElement(MakeSpeakerDevice(u"Vendor Model Analog Stereo"_ns, + u"Speaker-Model-GroupId"_ns)); + audios.AppendElement(MakeSpeakerDevice(u"Vendor Model Analog Stereo"_ns, + u"Speaker-Model-GroupId"_ns)); + + MediaManager::GuessVideoDeviceGroupIDs(devices, audios); + + EXPECT_EQ(devices[0]->mGroupID, Cam_Model_GroupId) + << "Video group id has not been updated."; + EXPECT_NE(devices[0]->mGroupID, audios[0]->mGroupID) + << "Video group id is different from audio input group id."; + EXPECT_NE(devices[0]->mGroupID, audios[2]->mGroupID) + << "Video group id is different from audio output group id."; +} + +/* Verify that when more that one audio input device name contain the video + * input device name the video device group id is not updated by audio input + * device group id but it continues looking at audio output devices where it + * finds a match so video input group id is updated by audio output group id. */ +TEST(TestGroupId, Match_TwoIdenticalInputsMatchOutput) +{ + MediaManager::MediaDeviceSet devices; + MediaManager::MediaDeviceSet audios; + + nsString Cam_Model_GroupId = u"Cam-Model-GroupId"_ns; + devices.AppendElement( + MakeCameraDevice(u"Vendor Model"_ns, Cam_Model_GroupId)); + + audios.AppendElement( + MakeMicDevice(u"Vendor Model Analog Stereo"_ns, u"Mic-Model-GroupId"_ns)); + audios.AppendElement( + MakeMicDevice(u"Vendor Model Analog Stereo"_ns, u"Mic-Model-GroupId"_ns)); + + audios.AppendElement(MakeSpeakerDevice(u"Vendor Model Analog Stereo"_ns, + u"Speaker-Model-GroupId"_ns)); + + MediaManager::GuessVideoDeviceGroupIDs(devices, audios); + + EXPECT_EQ(devices[0]->mGroupID, audios[2]->mGroupID) + << "Video group id is the same as audio output group id."; +} + +/* Verify that when more that one audio input and more than one audio output + * device names contain the video input device name the video device group id + * does not change. */ +TEST(TestGroupId, NoMatch_ThreeIdenticalDevices) +{ + MediaManager::MediaDeviceSet devices; + MediaManager::MediaDeviceSet audios; + + nsString Cam_Model_GroupId = u"Cam-Model-GroupId"_ns; + devices.AppendElement( + MakeCameraDevice(u"Vendor Model"_ns, Cam_Model_GroupId)); + + audios.AppendElement( + MakeMicDevice(u"Vendor Model Analog Stereo"_ns, u"Mic-Model-GroupId"_ns)); + audios.AppendElement( + MakeMicDevice(u"Vendor Model Analog Stereo"_ns, u"Mic-Model-GroupId"_ns)); + audios.AppendElement( + MakeMicDevice(u"Vendor Model Analog Stereo"_ns, u"Mic-Model-GroupId"_ns)); + + audios.AppendElement(MakeSpeakerDevice(u"Vendor Model Analog Stereo"_ns, + u"Speaker-Model-GroupId"_ns)); + audios.AppendElement(MakeSpeakerDevice(u"Vendor Model Analog Stereo"_ns, + u"Speaker-Model-GroupId"_ns)); + audios.AppendElement(MakeSpeakerDevice(u"Vendor Model Analog Stereo"_ns, + u"Speaker-Model-GroupId"_ns)); + + MediaManager::GuessVideoDeviceGroupIDs(devices, audios); + + EXPECT_EQ(devices[0]->mGroupID, Cam_Model_GroupId) + << "Video group id has not been updated."; + EXPECT_NE(devices[0]->mGroupID, audios[0]->mGroupID) + << "Video group id is different from audio input group id."; + EXPECT_NE(devices[0]->mGroupID, audios[3]->mGroupID) + << "Video group id is different from audio output group id."; +} + +/* Verify that when an audio output device name contains the video input device + * name the video device group id is updated to become equal to the audio + * device group id. */ +TEST(TestGroupId, MatchOutput) +{ + MediaManager::MediaDeviceSet devices; + MediaManager::MediaDeviceSet audios; + + devices.AppendElement( + MakeCameraDevice(u"Vendor Model"_ns, u"Cam-Model-GroupId"_ns)); + + audios.AppendElement( + MakeMicDevice(u"Mic Analog Stereo"_ns, u"Mic-Model-GroupId"_ns)); + + audios.AppendElement(MakeSpeakerDevice(u"Vendor Model Analog Stereo"_ns, + u"Speaker-Model-GroupId"_ns)); + + MediaManager::GuessVideoDeviceGroupIDs(devices, audios); + + EXPECT_EQ(devices[0]->mGroupID, audios[1]->mGroupID) + << "Video group id is the same as audio output group id."; +} + +/* Verify that when an audio input device name is the same as audio output + * device and video input device name the video device group id is updated to + * become equal to the audio input device group id. */ +TEST(TestGroupId, InputOutputSameName) +{ + MediaManager::MediaDeviceSet devices; + MediaManager::MediaDeviceSet audios; + + devices.AppendElement( + MakeCameraDevice(u"Vendor Model"_ns, u"Cam-Model-GroupId"_ns)); + + audios.AppendElement( + MakeMicDevice(u"Vendor Model"_ns, u"Mic-Model-GroupId"_ns)); + + audios.AppendElement( + MakeSpeakerDevice(u"Vendor Model"_ns, u"Speaker-Model-GroupId"_ns)); + + MediaManager::GuessVideoDeviceGroupIDs(devices, audios); + + EXPECT_EQ(devices[0]->mGroupID, audios[0]->mGroupID) + << "Video input group id is the same as audio input group id."; +} + +/* Verify that when an audio input device name contains the video input device + * and the audio input group id is an empty string, the video device group id + * is updated to become equal to the audio device group id. */ +TEST(TestGroupId, InputEmptyGroupId) +{ + MediaManager::MediaDeviceSet devices; + MediaManager::MediaDeviceSet audios; + + devices.AppendElement( + MakeCameraDevice(u"Vendor Model"_ns, u"Cam-Model-GroupId"_ns)); + + audios.AppendElement(MakeMicDevice(u"Vendor Model"_ns, u""_ns)); + + MediaManager::GuessVideoDeviceGroupIDs(devices, audios); + + EXPECT_EQ(devices[0]->mGroupID, audios[0]->mGroupID) + << "Video input group id is the same as audio input group id."; +} + +/* Verify that when an audio output device name contains the video input device + * and the audio output group id is an empty string, the video device group id + * is updated to become equal to the audio output device group id. */ +TEST(TestGroupId, OutputEmptyGroupId) +{ + MediaManager::MediaDeviceSet devices; + MediaManager::MediaDeviceSet audios; + + devices.AppendElement( + MakeCameraDevice(u"Vendor Model"_ns, u"Cam-Model-GroupId"_ns)); + + audios.AppendElement(MakeSpeakerDevice(u"Vendor Model"_ns, u""_ns)); + + MediaManager::GuessVideoDeviceGroupIDs(devices, audios); + + EXPECT_EQ(devices[0]->mGroupID, audios[0]->mGroupID) + << "Video input group id is the same as audio output group id."; +} diff --git a/dom/media/gtest/TestIntervalSet.cpp b/dom/media/gtest/TestIntervalSet.cpp new file mode 100644 index 0000000000..11d0428f6c --- /dev/null +++ b/dom/media/gtest/TestIntervalSet.cpp @@ -0,0 +1,819 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/TimeRanges.h" +#include "TimeUnits.h" +#include "Intervals.h" +#include <algorithm> +#include <type_traits> +#include <vector> + +using namespace mozilla; + +typedef media::Interval<uint8_t> ByteInterval; +typedef media::Interval<int> IntInterval; +typedef media::IntervalSet<int> IntIntervals; + +ByteInterval CreateByteInterval(int32_t aStart, int32_t aEnd) { + ByteInterval test(aStart, aEnd); + return test; +} + +media::IntervalSet<uint8_t> CreateByteIntervalSet(int32_t aStart, + int32_t aEnd) { + media::IntervalSet<uint8_t> test; + test += ByteInterval(aStart, aEnd); + return test; +} + +TEST(IntervalSet, Constructors) +{ + const int32_t start = 1; + const int32_t end = 2; + const int32_t fuzz = 0; + + // Compiler exercise. + ByteInterval test1(start, end); + ByteInterval test2(test1); + ByteInterval test3(start, end, fuzz); + ByteInterval test4(test3); + ByteInterval test5 = CreateByteInterval(start, end); + + media::IntervalSet<uint8_t> blah1(test1); + media::IntervalSet<uint8_t> blah2 = blah1; + media::IntervalSet<uint8_t> blah3 = blah1 + test1; + media::IntervalSet<uint8_t> blah4 = test1 + blah1; + media::IntervalSet<uint8_t> blah5 = CreateByteIntervalSet(start, end); + (void)test1; + (void)test2; + (void)test3; + (void)test4; + (void)test5; + (void)blah1; + (void)blah2; + (void)blah3; + (void)blah4; + (void)blah5; +} + +media::TimeInterval CreateTimeInterval(int32_t aStart, int32_t aEnd) { + // Copy constructor test + media::TimeUnit start = media::TimeUnit::FromMicroseconds(aStart); + media::TimeUnit end; + // operator= test + end = media::TimeUnit::FromMicroseconds(aEnd); + media::TimeInterval ti(start, end); + return ti; +} + +media::TimeIntervals CreateTimeIntervals(int32_t aStart, int32_t aEnd) { + media::TimeIntervals test; + test += CreateTimeInterval(aStart, aEnd); + return test; +} + +TEST(IntervalSet, TimeIntervalsConstructors) +{ + const auto start = media::TimeUnit::FromMicroseconds(1); + const auto end = media::TimeUnit::FromMicroseconds(2); + const media::TimeUnit fuzz; + + // Compiler exercise. + media::TimeInterval test1(start, end); + media::TimeInterval test2(test1); + media::TimeInterval test3(start, end, fuzz); + media::TimeInterval test4(test3); + media::TimeInterval test5 = + CreateTimeInterval(start.ToMicroseconds(), end.ToMicroseconds()); + + media::TimeIntervals blah1(test1); + media::TimeIntervals blah2(blah1); + media::TimeIntervals blah3 = blah1 + test1; + media::TimeIntervals blah4 = test1 + blah1; + media::TimeIntervals blah5 = + CreateTimeIntervals(start.ToMicroseconds(), end.ToMicroseconds()); + (void)test1; + (void)test2; + (void)test3; + (void)test4; + (void)test5; + (void)blah1; + (void)blah2; + (void)blah3; + (void)blah4; + (void)blah5; + + media::TimeIntervals i0{media::TimeInterval(media::TimeUnit::FromSeconds(0), + media::TimeUnit::FromSeconds(0))}; + EXPECT_TRUE(i0.IsEmpty()); // Constructing with an empty time interval. +} + +TEST(IntervalSet, Length) +{ + IntInterval i(15, 25); + EXPECT_EQ(10, i.Length()); +} + +TEST(IntervalSet, Intersects) +{ + EXPECT_TRUE(IntInterval(1, 5).Intersects(IntInterval(3, 4))); + EXPECT_TRUE(IntInterval(1, 5).Intersects(IntInterval(3, 7))); + EXPECT_TRUE(IntInterval(1, 5).Intersects(IntInterval(-1, 3))); + EXPECT_TRUE(IntInterval(1, 5).Intersects(IntInterval(-1, 7))); + EXPECT_FALSE(IntInterval(1, 5).Intersects(IntInterval(6, 7))); + EXPECT_FALSE(IntInterval(1, 5).Intersects(IntInterval(-1, 0))); + // End boundary is exclusive of the interval. + EXPECT_FALSE(IntInterval(1, 5).Intersects(IntInterval(5, 7))); + EXPECT_FALSE(IntInterval(1, 5).Intersects(IntInterval(0, 1))); + // Empty identical interval do not intersect. + EXPECT_FALSE(IntInterval(1, 1).Intersects(IntInterval(1, 1))); + // Empty interval do not intersect. + EXPECT_FALSE(IntInterval(1, 1).Intersects(IntInterval(2, 2))); +} + +TEST(IntervalSet, Intersection) +{ + IntInterval i0(10, 20); + IntInterval i1(15, 25); + IntInterval i = i0.Intersection(i1); + EXPECT_EQ(15, i.mStart); + EXPECT_EQ(20, i.mEnd); + IntInterval j0(10, 20); + IntInterval j1(20, 25); + IntInterval j = j0.Intersection(j1); + EXPECT_TRUE(j.IsEmpty()); + IntInterval k0(2, 2); + IntInterval k1(2, 2); + IntInterval k = k0.Intersection(k1); + EXPECT_TRUE(k.IsEmpty()); +} + +TEST(IntervalSet, Equals) +{ + IntInterval i0(10, 20); + IntInterval i1(10, 20); + EXPECT_EQ(i0, i1); + + IntInterval i2(5, 20); + EXPECT_NE(i0, i2); + + IntInterval i3(10, 15); + EXPECT_NE(i0, i2); +} + +TEST(IntervalSet, IntersectionIntervalSet) +{ + IntIntervals i0; + i0 += IntInterval(5, 10); + i0 += IntInterval(20, 25); + i0 += IntInterval(40, 60); + + IntIntervals i1; + i1.Add(IntInterval(7, 15)); + i1.Add(IntInterval(16, 27)); + i1.Add(IntInterval(45, 50)); + i1.Add(IntInterval(53, 57)); + + IntIntervals i = media::Intersection(i0, i1); + + EXPECT_EQ(4u, i.Length()); + + EXPECT_EQ(7, i[0].mStart); + EXPECT_EQ(10, i[0].mEnd); + + EXPECT_EQ(20, i[1].mStart); + EXPECT_EQ(25, i[1].mEnd); + + EXPECT_EQ(45, i[2].mStart); + EXPECT_EQ(50, i[2].mEnd); + + EXPECT_EQ(53, i[3].mStart); + EXPECT_EQ(57, i[3].mEnd); +} + +template <typename T> +static void Compare(const media::IntervalSet<T>& aI1, + const media::IntervalSet<T>& aI2) { + EXPECT_EQ(aI1.Length(), aI2.Length()); + if (aI1.Length() != aI2.Length()) { + return; + } + for (uint32_t i = 0; i < aI1.Length(); i++) { + EXPECT_EQ(aI1[i].mStart, aI2[i].mStart); + EXPECT_EQ(aI1[i].mEnd, aI2[i].mEnd); + } +} + +static void GeneratePermutations(const IntIntervals& aI1, + const IntIntervals& aI2) { + IntIntervals i_ref = media::Intersection(aI1, aI2); + // Test all permutations possible + std::vector<uint32_t> comb1; + for (uint32_t i = 0; i < aI1.Length(); i++) { + comb1.push_back(i); + } + std::vector<uint32_t> comb2; + for (uint32_t i = 0; i < aI2.Length(); i++) { + comb2.push_back(i); + } + + do { + do { + // Create intervals according to new indexes. + IntIntervals i_0; + for (uint32_t i = 0; i < comb1.size(); i++) { + i_0 += aI1[comb1[i]]; + } + // Test that intervals are always normalized. + Compare(aI1, i_0); + IntIntervals i_1; + for (uint32_t i = 0; i < comb2.size(); i++) { + i_1 += aI2[comb2[i]]; + } + Compare(aI2, i_1); + // Check intersections yield the same result. + Compare(i_0.Intersection(i_1), i_ref); + } while (std::next_permutation(comb2.begin(), comb2.end())); + } while (std::next_permutation(comb1.begin(), comb1.end())); +} + +TEST(IntervalSet, IntersectionNormalizedIntervalSet) +{ + IntIntervals i0; + i0 += IntInterval(5, 10); + i0 += IntInterval(20, 25); + i0 += IntInterval(40, 60); + + IntIntervals i1; + i1.Add(IntInterval(7, 15)); + i1.Add(IntInterval(16, 27)); + i1.Add(IntInterval(45, 50)); + i1.Add(IntInterval(53, 57)); + + GeneratePermutations(i0, i1); +} + +TEST(IntervalSet, IntersectionUnorderedNonNormalizedIntervalSet) +{ + IntIntervals i0; + i0 += IntInterval(5, 10); + i0 += IntInterval(8, 25); + i0 += IntInterval(24, 60); + + IntIntervals i1; + i1.Add(IntInterval(7, 15)); + i1.Add(IntInterval(10, 27)); + i1.Add(IntInterval(45, 50)); + i1.Add(IntInterval(53, 57)); + + GeneratePermutations(i0, i1); +} + +TEST(IntervalSet, IntersectionNonNormalizedInterval) +{ + IntIntervals i0; + i0 += IntInterval(5, 10); + i0 += IntInterval(8, 25); + i0 += IntInterval(30, 60); + + media::Interval<int> i1(9, 15); + i0.Intersection(i1); + EXPECT_EQ(1u, i0.Length()); + EXPECT_EQ(i0[0].mStart, i1.mStart); + EXPECT_EQ(i0[0].mEnd, i1.mEnd); +} + +TEST(IntervalSet, IntersectionUnorderedNonNormalizedInterval) +{ + IntIntervals i0; + i0 += IntInterval(1, 3); + i0 += IntInterval(1, 10); + i0 += IntInterval(9, 12); + i0 += IntInterval(12, 15); + i0 += IntInterval(8, 25); + i0 += IntInterval(30, 60); + i0 += IntInterval(5, 10); + i0 += IntInterval(30, 60); + + media::Interval<int> i1(9, 15); + i0.Intersection(i1); + EXPECT_EQ(1u, i0.Length()); + EXPECT_EQ(i0[0].mStart, i1.mStart); + EXPECT_EQ(i0[0].mEnd, i1.mEnd); +} + +static IntIntervals Duplicate(const IntIntervals& aValue) { + IntIntervals value(aValue); + return value; +} + +TEST(IntervalSet, Normalize) +{ + IntIntervals i; + // Test IntervalSet<T> + Interval<T> operator. + i = i + IntInterval(20, 30); + // Test Internal<T> + IntervalSet<T> operator. + i = IntInterval(2, 7) + i; + // Test Interval<T> + IntervalSet<T> operator + i = IntInterval(1, 8) + i; + IntIntervals interval; + interval += IntInterval(5, 10); + // Test += with rval move. + i += Duplicate(interval); + // Test = with move and add with move. + i = Duplicate(interval) + i; + + EXPECT_EQ(2u, i.Length()); + + EXPECT_EQ(1, i[0].mStart); + EXPECT_EQ(10, i[0].mEnd); + + EXPECT_EQ(20, i[1].mStart); + EXPECT_EQ(30, i[1].mEnd); + + media::TimeIntervals ti; + ti += media::TimeInterval(media::TimeUnit::FromSeconds(0.0), + media::TimeUnit::FromSeconds(3.203333)); + ti += media::TimeInterval(media::TimeUnit::FromSeconds(3.203366), + media::TimeUnit::FromSeconds(10.010065)); + EXPECT_EQ(2u, ti.Length()); + ti += media::TimeInterval(ti.Start(0), ti.End(0), + media::TimeUnit::FromMicroseconds(35000)); + EXPECT_EQ(1u, ti.Length()); +} + +TEST(IntervalSet, ContainValue) +{ + IntIntervals i0; + i0 += IntInterval(0, 10); + i0 += IntInterval(15, 20); + i0 += IntInterval(30, 50); + EXPECT_TRUE(i0.Contains(0)); // start is inclusive. + EXPECT_TRUE(i0.Contains(17)); + EXPECT_FALSE(i0.Contains(20)); // end boundary is exclusive. + EXPECT_FALSE(i0.Contains(25)); +} + +TEST(IntervalSet, ContainValueWithFuzz) +{ + IntIntervals i0; + i0 += IntInterval(0, 10); + i0 += IntInterval(15, 20, 1); + i0 += IntInterval(30, 50); + EXPECT_TRUE(i0.Contains(0)); // start is inclusive. + EXPECT_TRUE(i0.Contains(17)); + EXPECT_TRUE( + i0.Contains(20)); // end boundary is exclusive but we have a fuzz of 1. + EXPECT_FALSE(i0.Contains(25)); +} + +TEST(IntervalSet, ContainInterval) +{ + IntIntervals i0; + i0 += IntInterval(0, 10); + i0 += IntInterval(15, 20); + i0 += IntInterval(30, 50); + EXPECT_TRUE(i0.Contains(IntInterval(2, 8))); + EXPECT_TRUE(i0.Contains(IntInterval(31, 50))); + EXPECT_TRUE(i0.Contains(IntInterval(0, 10))); + EXPECT_FALSE(i0.Contains(IntInterval(0, 11))); + EXPECT_TRUE(i0.Contains(IntInterval(0, 5))); + EXPECT_FALSE(i0.Contains(IntInterval(8, 15))); + EXPECT_FALSE(i0.Contains(IntInterval(15, 30))); + EXPECT_FALSE(i0.Contains(IntInterval(30, 55))); +} + +TEST(IntervalSet, ContainIntervalWithFuzz) +{ + IntIntervals i0; + i0 += IntInterval(0, 10); + i0 += IntInterval(15, 20); + i0 += IntInterval(30, 50); + EXPECT_TRUE(i0.Contains(IntInterval(2, 8))); + EXPECT_TRUE(i0.Contains(IntInterval(31, 50))); + EXPECT_TRUE(i0.Contains(IntInterval(0, 11, 1))); + EXPECT_TRUE(i0.Contains(IntInterval(0, 5))); + EXPECT_FALSE(i0.Contains(IntInterval(8, 15))); + EXPECT_FALSE(i0.Contains(IntInterval(15, 21))); + EXPECT_FALSE(i0.Contains(IntInterval(15, 30))); + EXPECT_FALSE(i0.Contains(IntInterval(30, 55))); + + IntIntervals i1; + i1 += IntInterval(0, 10, 1); + i1 += IntInterval(15, 20, 1); + i1 += IntInterval(30, 50, 1); + EXPECT_TRUE(i1.Contains(IntInterval(2, 8))); + EXPECT_TRUE(i1.Contains(IntInterval(29, 51))); + EXPECT_TRUE(i1.Contains(IntInterval(0, 11, 1))); + EXPECT_TRUE(i1.Contains(IntInterval(15, 21))); +} + +TEST(IntervalSet, Span) +{ + IntInterval i0(0, 10); + IntInterval i1(20, 30); + IntInterval i{i0.Span(i1)}; + + EXPECT_EQ(i.mStart, 0); + EXPECT_EQ(i.mEnd, 30); +} + +TEST(IntervalSet, Union) +{ + IntIntervals i0; + i0 += IntInterval(5, 10); + i0 += IntInterval(20, 25); + i0 += IntInterval(40, 60); + + IntIntervals i1; + i1.Add(IntInterval(7, 15)); + i1.Add(IntInterval(16, 27)); + i1.Add(IntInterval(45, 50)); + i1.Add(IntInterval(53, 57)); + + IntIntervals i = media::Union(i0, i1); + + EXPECT_EQ(3u, i.Length()); + + EXPECT_EQ(5, i[0].mStart); + EXPECT_EQ(15, i[0].mEnd); + + EXPECT_EQ(16, i[1].mStart); + EXPECT_EQ(27, i[1].mEnd); + + EXPECT_EQ(40, i[2].mStart); + EXPECT_EQ(60, i[2].mEnd); +} + +TEST(IntervalSet, UnionNotOrdered) +{ + IntIntervals i0; + i0 += IntInterval(20, 25); + i0 += IntInterval(40, 60); + i0 += IntInterval(5, 10); + + IntIntervals i1; + i1.Add(IntInterval(16, 27)); + i1.Add(IntInterval(7, 15)); + i1.Add(IntInterval(53, 57)); + i1.Add(IntInterval(45, 50)); + + IntIntervals i = media::Union(i0, i1); + + EXPECT_EQ(3u, i.Length()); + + EXPECT_EQ(5, i[0].mStart); + EXPECT_EQ(15, i[0].mEnd); + + EXPECT_EQ(16, i[1].mStart); + EXPECT_EQ(27, i[1].mEnd); + + EXPECT_EQ(40, i[2].mStart); + EXPECT_EQ(60, i[2].mEnd); +} + +TEST(IntervalSet, NormalizeFuzz) +{ + IntIntervals i0; + i0 += IntInterval(11, 25, 0); + i0 += IntInterval(5, 10, 1); + i0 += IntInterval(40, 60, 1); + + EXPECT_EQ(2u, i0.Length()); + + EXPECT_EQ(5, i0[0].mStart); + EXPECT_EQ(25, i0[0].mEnd); + + EXPECT_EQ(40, i0[1].mStart); + EXPECT_EQ(60, i0[1].mEnd); +} + +TEST(IntervalSet, UnionFuzz) +{ + IntIntervals i0; + i0 += IntInterval(5, 10, 1); + i0 += IntInterval(11, 25, 0); + i0 += IntInterval(40, 60, 1); + EXPECT_EQ(2u, i0.Length()); + EXPECT_EQ(5, i0[0].mStart); + EXPECT_EQ(25, i0[0].mEnd); + EXPECT_EQ(40, i0[1].mStart); + EXPECT_EQ(60, i0[1].mEnd); + + IntIntervals i1; + i1.Add(IntInterval(7, 15, 1)); + i1.Add(IntInterval(16, 27, 1)); + i1.Add(IntInterval(45, 50, 1)); + i1.Add(IntInterval(53, 57, 1)); + EXPECT_EQ(3u, i1.Length()); + EXPECT_EQ(7, i1[0].mStart); + EXPECT_EQ(27, i1[0].mEnd); + EXPECT_EQ(45, i1[1].mStart); + EXPECT_EQ(50, i1[1].mEnd); + EXPECT_EQ(53, i1[2].mStart); + EXPECT_EQ(57, i1[2].mEnd); + + IntIntervals i = media::Union(i0, i1); + + EXPECT_EQ(2u, i.Length()); + + EXPECT_EQ(5, i[0].mStart); + EXPECT_EQ(27, i[0].mEnd); + + EXPECT_EQ(40, i[1].mStart); + EXPECT_EQ(60, i[1].mEnd); +} + +TEST(IntervalSet, Contiguous) +{ + EXPECT_FALSE(IntInterval(5, 10).Contiguous(IntInterval(11, 25))); + EXPECT_TRUE(IntInterval(5, 10).Contiguous(IntInterval(10, 25))); + EXPECT_TRUE(IntInterval(5, 10, 1).Contiguous(IntInterval(11, 25))); + EXPECT_TRUE(IntInterval(5, 10).Contiguous(IntInterval(11, 25, 1))); +} + +TEST(IntervalSet, TimeRangesSeconds) +{ + media::TimeIntervals i0; + i0 += media::TimeInterval(media::TimeUnit::FromSeconds(20), + media::TimeUnit::FromSeconds(25)); + i0 += media::TimeInterval(media::TimeUnit::FromSeconds(40), + media::TimeUnit::FromSeconds(60)); + i0 += media::TimeInterval(media::TimeUnit::FromSeconds(5), + media::TimeUnit::FromSeconds(10)); + + media::TimeIntervals i1; + i1.Add(media::TimeInterval(media::TimeUnit::FromSeconds(16), + media::TimeUnit::FromSeconds(27))); + i1.Add(media::TimeInterval(media::TimeUnit::FromSeconds(7), + media::TimeUnit::FromSeconds(15))); + i1.Add(media::TimeInterval(media::TimeUnit::FromSeconds(53), + media::TimeUnit::FromSeconds(57))); + i1.Add(media::TimeInterval(media::TimeUnit::FromSeconds(45), + media::TimeUnit::FromSeconds(50))); + + media::TimeIntervals i(i0 + i1); + RefPtr<dom::TimeRanges> tr = new dom::TimeRanges(i); + EXPECT_EQ(tr->Length(), i.Length()); + for (dom::TimeRanges::index_type index = 0; index < tr->Length(); index++) { + ErrorResult rv; + EXPECT_EQ(tr->Start(index, rv), i[index].mStart.ToSeconds()); + EXPECT_EQ(tr->Start(index, rv), i.Start(index).ToSeconds()); + EXPECT_EQ(tr->End(index, rv), i[index].mEnd.ToSeconds()); + EXPECT_EQ(tr->End(index, rv), i.End(index).ToSeconds()); + } +} + +static void CheckTimeRanges(dom::TimeRanges* aTr, + const media::TimeIntervals& aTi) { + RefPtr<dom::TimeRanges> tr = new dom::TimeRanges; + tr->Union(aTr, 0); // This will normalize the time range. + EXPECT_EQ(tr->Length(), aTi.Length()); + for (dom::TimeRanges::index_type i = 0; i < tr->Length(); i++) { + ErrorResult rv; + EXPECT_EQ(tr->Start(i, rv), aTi[i].mStart.ToSeconds()); + EXPECT_EQ(tr->Start(i, rv), aTi.Start(i).ToSeconds()); + EXPECT_EQ(tr->End(i, rv), aTi[i].mEnd.ToSeconds()); + EXPECT_EQ(tr->End(i, rv), aTi.End(i).ToSeconds()); + } +} + +TEST(IntervalSet, TimeRangesConversion) +{ + RefPtr<dom::TimeRanges> tr = new dom::TimeRanges(); + tr->Add(20, 25); + tr->Add(40, 60); + tr->Add(5, 10); + tr->Add(16, 27); + tr->Add(53, 57); + tr->Add(45, 50); + + // explicit copy constructor and ToTimeIntervals. + media::TimeIntervals i1(tr->ToTimeIntervals()); + CheckTimeRanges(tr, i1); + + // ctor(const TimeIntervals&) + RefPtr<dom::TimeRanges> tr2 = new dom::TimeRanges(tr->ToTimeIntervals()); + CheckTimeRanges(tr2, i1); +} + +TEST(IntervalSet, TimeRangesMicroseconds) +{ + media::TimeIntervals i0; + + i0 += media::TimeInterval(media::TimeUnit::FromMicroseconds(20), + media::TimeUnit::FromMicroseconds(25)); + i0 += media::TimeInterval(media::TimeUnit::FromMicroseconds(40), + media::TimeUnit::FromMicroseconds(60)); + i0 += media::TimeInterval(media::TimeUnit::FromMicroseconds(5), + media::TimeUnit::FromMicroseconds(10)); + + media::TimeIntervals i1; + i1.Add(media::TimeInterval(media::TimeUnit::FromMicroseconds(16), + media::TimeUnit::FromMicroseconds(27))); + i1.Add(media::TimeInterval(media::TimeUnit::FromMicroseconds(7), + media::TimeUnit::FromMicroseconds(15))); + i1.Add(media::TimeInterval(media::TimeUnit::FromMicroseconds(53), + media::TimeUnit::FromMicroseconds(57))); + i1.Add(media::TimeInterval(media::TimeUnit::FromMicroseconds(45), + media::TimeUnit::FromMicroseconds(50))); + + media::TimeIntervals i(i0 + i1); + RefPtr<dom::TimeRanges> tr = new dom::TimeRanges(i); + EXPECT_EQ(tr->Length(), i.Length()); + for (dom::TimeRanges::index_type index = 0; index < tr->Length(); index++) { + ErrorResult rv; + EXPECT_EQ(tr->Start(index, rv), i[index].mStart.ToSeconds()); + EXPECT_EQ(tr->Start(index, rv), i.Start(index).ToSeconds()); + EXPECT_EQ(tr->End(index, rv), i[index].mEnd.ToSeconds()); + EXPECT_EQ(tr->End(index, rv), i.End(index).ToSeconds()); + } + + tr->Normalize(); + EXPECT_EQ(tr->Length(), i.Length()); + for (dom::TimeRanges::index_type index = 0; index < tr->Length(); index++) { + ErrorResult rv; + EXPECT_EQ(tr->Start(index, rv), i[index].mStart.ToSeconds()); + EXPECT_EQ(tr->Start(index, rv), i.Start(index).ToSeconds()); + EXPECT_EQ(tr->End(index, rv), i[index].mEnd.ToSeconds()); + EXPECT_EQ(tr->End(index, rv), i.End(index).ToSeconds()); + } + + // Check infinity values aren't lost in the conversion. + tr = new dom::TimeRanges(); + tr->Add(0, 30); + tr->Add(50, std::numeric_limits<double>::infinity()); + media::TimeIntervals i_oo = tr->ToTimeIntervals(); + RefPtr<dom::TimeRanges> tr2 = new dom::TimeRanges(i_oo); + EXPECT_EQ(tr->Length(), tr2->Length()); + for (dom::TimeRanges::index_type index = 0; index < tr->Length(); index++) { + ErrorResult rv; + EXPECT_EQ(tr->Start(index, rv), tr2->Start(index, rv)); + EXPECT_EQ(tr->End(index, rv), tr2->End(index, rv)); + } +} + +template <typename T> +class Foo { + public: + Foo() : mArg1(1), mArg2(2), mArg3(3) {} + + Foo(T a1, T a2, T a3) : mArg1(a1), mArg2(a2), mArg3(a3) {} + + Foo<T> operator+(const Foo<T>& aOther) const { + Foo<T> blah; + blah.mArg1 += aOther.mArg1; + blah.mArg2 += aOther.mArg2; + blah.mArg3 += aOther.mArg3; + return blah; + } + Foo<T> operator-(const Foo<T>& aOther) const { + Foo<T> blah; + blah.mArg1 -= aOther.mArg1; + blah.mArg2 -= aOther.mArg2; + blah.mArg3 -= aOther.mArg3; + return blah; + } + bool operator<(const Foo<T>& aOther) const { return mArg1 < aOther.mArg1; } + bool operator==(const Foo<T>& aOther) const { return mArg1 == aOther.mArg1; } + bool operator<=(const Foo<T>& aOther) const { return mArg1 <= aOther.mArg1; } + + private: + int32_t mArg1; + int32_t mArg2; + int32_t mArg3; +}; + +TEST(IntervalSet, FooIntervalSet) +{ + media::Interval<Foo<int>> i(Foo<int>(), Foo<int>(4, 5, 6)); + media::IntervalSet<Foo<int>> is; + is += i; + is += i; + is.Add(i); + is = is + i; + is = i + is; + EXPECT_EQ(1u, is.Length()); + EXPECT_EQ(Foo<int>(), is[0].mStart); + EXPECT_EQ(Foo<int>(4, 5, 6), is[0].mEnd); +} + +TEST(IntervalSet, StaticAssert) +{ + media::Interval<int> i; + + static_assert( + std::is_same_v<nsTArray_RelocationStrategy<IntIntervals>::Type, + nsTArray_RelocateUsingMoveConstructor<IntIntervals>>, + "Must use copy constructor"); + static_assert( + std::is_same_v< + nsTArray_RelocationStrategy<media::TimeIntervals>::Type, + nsTArray_RelocateUsingMoveConstructor<media::TimeIntervals>>, + "Must use copy constructor"); +} + +TEST(IntervalSet, Substraction) +{ + IntIntervals i0; + i0 += IntInterval(5, 10); + i0 += IntInterval(20, 25); + i0 += IntInterval(40, 60); + + IntInterval i1(8, 15); + i0 -= i1; + + EXPECT_EQ(3u, i0.Length()); + EXPECT_EQ(5, i0[0].mStart); + EXPECT_EQ(8, i0[0].mEnd); + EXPECT_EQ(20, i0[1].mStart); + EXPECT_EQ(25, i0[1].mEnd); + EXPECT_EQ(40, i0[2].mStart); + EXPECT_EQ(60, i0[2].mEnd); + + i0 = IntIntervals(); + i0 += IntInterval(5, 10); + i0 += IntInterval(20, 25); + i0 += IntInterval(40, 60); + i1 = IntInterval(0, 60); + i0 -= i1; + EXPECT_TRUE(i0.IsEmpty()); + + i0 = IntIntervals(); + i0 += IntInterval(5, 10); + i0 += IntInterval(20, 25); + i0 += IntInterval(40, 60); + i1 = IntInterval(0, 45); + i0 -= i1; + EXPECT_EQ(1u, i0.Length()); + EXPECT_EQ(45, i0[0].mStart); + EXPECT_EQ(60, i0[0].mEnd); + + i0 = IntIntervals(); + i0 += IntInterval(5, 10); + i0 += IntInterval(20, 25); + i0 += IntInterval(40, 60); + i1 = IntInterval(8, 45); + i0 -= i1; + EXPECT_EQ(2u, i0.Length()); + EXPECT_EQ(5, i0[0].mStart); + EXPECT_EQ(8, i0[0].mEnd); + EXPECT_EQ(45, i0[1].mStart); + EXPECT_EQ(60, i0[1].mEnd); + + i0 = IntIntervals(); + i0 += IntInterval(5, 10); + i0 += IntInterval(20, 25); + i0 += IntInterval(40, 60); + i1 = IntInterval(8, 70); + i0 -= i1; + EXPECT_EQ(1u, i0.Length()); + EXPECT_EQ(5, i0[0].mStart); + EXPECT_EQ(8, i0[0].mEnd); + + i0 = IntIntervals(); + i0 += IntInterval(0, 10); + IntIntervals i2; + i2 += IntInterval(4, 6); + i0 -= i2; + EXPECT_EQ(2u, i0.Length()); + EXPECT_EQ(0, i0[0].mStart); + EXPECT_EQ(4, i0[0].mEnd); + EXPECT_EQ(6, i0[1].mStart); + EXPECT_EQ(10, i0[1].mEnd); + + i0 = IntIntervals(); + i0 += IntInterval(0, 1); + i0 += IntInterval(3, 10); + EXPECT_EQ(2u, i0.Length()); + // This fuzz should collapse i0 into [0,10). + i0.SetFuzz(1); + EXPECT_EQ(1u, i0.Length()); + EXPECT_EQ(1, i0[0].mFuzz); + i2 = IntInterval(4, 6); + i0 -= i2; + EXPECT_EQ(2u, i0.Length()); + EXPECT_EQ(0, i0[0].mStart); + EXPECT_EQ(4, i0[0].mEnd); + EXPECT_EQ(6, i0[1].mStart); + EXPECT_EQ(10, i0[1].mEnd); + EXPECT_EQ(1, i0[0].mFuzz); + EXPECT_EQ(1, i0[1].mFuzz); + + i0 = IntIntervals(); + i0 += IntInterval(0, 10); + // [4,6) with fuzz 1 used to fail because the complementary interval set + // [0,4)+[6,10) would collapse into [0,10). + i2 = IntInterval(4, 6); + i2.SetFuzz(1); + i0 -= i2; + EXPECT_EQ(2u, i0.Length()); + EXPECT_EQ(0, i0[0].mStart); + EXPECT_EQ(4, i0[0].mEnd); + EXPECT_EQ(6, i0[1].mStart); + EXPECT_EQ(10, i0[1].mEnd); +} diff --git a/dom/media/gtest/TestKeyValueStorage.cpp b/dom/media/gtest/TestKeyValueStorage.cpp new file mode 100644 index 0000000000..7ba65343e3 --- /dev/null +++ b/dom/media/gtest/TestKeyValueStorage.cpp @@ -0,0 +1,109 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/KeyValueStorage.h" + +#include "gmock/gmock.h" +#include "gtest/gtest-printers.h" +#include "gtest/gtest.h" + +#include "GMPTestMonitor.h" + +using ::testing::Return; +using namespace mozilla; + +TEST(TestKeyValueStorage, BasicPutGet) +{ + auto kvs = MakeRefPtr<KeyValueStorage>(); + + nsCString name("database_name"); + nsCString key("key1"); + int32_t value = 100; + + GMPTestMonitor mon; + + kvs->Put(name, key, value) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [&](bool) { return kvs->Get(name, key); }, + [](nsresult rv) { + EXPECT_TRUE(false) << "Put promise has been rejected"; + return KeyValueStorage::GetPromise::CreateAndReject(rv, __func__); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [&](int32_t aValue) { + EXPECT_EQ(aValue, value) << "Values are the same"; + mon.SetFinished(); + }, + [&](nsresult rv) { + EXPECT_TRUE(false) << "Get Promise has been rejected"; + mon.SetFinished(); + }); + + mon.AwaitFinished(); +} + +TEST(TestKeyValueStorage, GetNonExistedKey) +{ + auto kvs = MakeRefPtr<KeyValueStorage>(); + + nsCString name("database_name"); + nsCString key("NonExistedKey"); + + GMPTestMonitor mon; + + kvs->Get(name, key)->Then( + GetCurrentSerialEventTarget(), __func__, + [&mon](int32_t aValue) { + EXPECT_EQ(aValue, -1) << "When key does not exist return -1"; + mon.SetFinished(); + }, + [&mon](nsresult rv) { + EXPECT_TRUE(false) << "Get Promise has been rejected"; + mon.SetFinished(); + }); + + mon.AwaitFinished(); +} + +TEST(TestKeyValueStorage, Clear) +{ + auto kvs = MakeRefPtr<KeyValueStorage>(); + + nsCString name("database_name"); + nsCString key("key1"); + int32_t value = 100; + + GMPTestMonitor mon; + + kvs->Put(name, key, value) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [&](bool) { return kvs->Clear(name); }, + [](nsresult rv) { + EXPECT_TRUE(false) << "Put promise has been rejected"; + return GenericPromise::CreateAndReject(rv, __func__); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [&](bool) { return kvs->Get(name, key); }, + [](nsresult rv) { + EXPECT_TRUE(false) << "Clear promise has been rejected"; + return KeyValueStorage::GetPromise::CreateAndReject(rv, __func__); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [&](int32_t aValue) { + EXPECT_EQ(aValue, -1) << "After clear the key does not exist"; + mon.SetFinished(); + }, + [&](nsresult rv) { + EXPECT_TRUE(false) << "Get Promise has been rejected"; + mon.SetFinished(); + }); + + mon.AwaitFinished(); +} diff --git a/dom/media/gtest/TestMP3Demuxer.cpp b/dom/media/gtest/TestMP3Demuxer.cpp new file mode 100644 index 0000000000..16de384572 --- /dev/null +++ b/dom/media/gtest/TestMP3Demuxer.cpp @@ -0,0 +1,559 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <gtest/gtest.h> +#include <vector> + +#include "MP3Demuxer.h" +#include "mozilla/ArrayUtils.h" +#include "MockMediaResource.h" + +class MockMP3MediaResource; +class MockMP3StreamMediaResource; +namespace mozilla { +DDLoggedTypeNameAndBase(::MockMP3MediaResource, MockMediaResource); +DDLoggedTypeNameAndBase(::MockMP3StreamMediaResource, MockMP3MediaResource); +} // namespace mozilla + +using namespace mozilla; +using media::TimeUnit; + +// Regular MP3 file mock resource. +class MockMP3MediaResource + : public MockMediaResource, + public DecoderDoctorLifeLogger<MockMP3MediaResource> { + public: + explicit MockMP3MediaResource(const char* aFileName) + : MockMediaResource(aFileName) {} + + protected: + virtual ~MockMP3MediaResource() = default; +}; + +// MP3 stream mock resource. +class MockMP3StreamMediaResource + : public MockMP3MediaResource, + public DecoderDoctorLifeLogger<MockMP3StreamMediaResource> { + public: + explicit MockMP3StreamMediaResource(const char* aFileName) + : MockMP3MediaResource(aFileName) {} + + int64_t GetLength() override { return -1; } + + protected: + virtual ~MockMP3StreamMediaResource() = default; +}; + +struct MP3Resource { + enum class HeaderType { NONE, XING, VBRI }; + struct Duration { + int64_t mMicroseconds; + float mTolerableRate; + + Duration(int64_t aMicroseconds, float aTolerableRate) + : mMicroseconds(aMicroseconds), mTolerableRate(aTolerableRate) {} + int64_t Tolerance() const { return mTolerableRate * mMicroseconds; } + }; + + const char* mFilePath; + bool mIsVBR; + HeaderType mHeaderType; + int64_t mFileSize; + int32_t mMPEGLayer; + int32_t mMPEGVersion; + uint8_t mID3MajorVersion; + uint8_t mID3MinorVersion; + uint8_t mID3Flags; + uint32_t mID3Size; + + Maybe<Duration> mDuration; + float mSeekError; + int32_t mSampleRate; + int32_t mSamplesPerFrame; + uint32_t mNumSamples; + // TODO: temp solution, we could parse them instead or account for them + // otherwise. + int32_t mNumTrailingFrames; + int32_t mBitrate; + int32_t mSlotSize; + int32_t mPrivate; + + // The first n frame offsets. + std::vector<int32_t> mSyncOffsets; + RefPtr<MockMP3MediaResource> mResource; + RefPtr<MP3TrackDemuxer> mDemuxer; +}; + +class MP3DemuxerTest : public ::testing::Test { + protected: + void SetUp() override { + { + MP3Resource res; + res.mFilePath = "noise.mp3"; + res.mIsVBR = false; + res.mHeaderType = MP3Resource::HeaderType::NONE; + res.mFileSize = 965257; + res.mMPEGLayer = 3; + res.mMPEGVersion = 1; + res.mID3MajorVersion = 3; + res.mID3MinorVersion = 0; + res.mID3Flags = 0; + res.mID3Size = 2141; + res.mDuration = Some(MP3Resource::Duration{30067000, 0.001f}); + res.mSeekError = 0.02f; + res.mSampleRate = 44100; + res.mSamplesPerFrame = 1152; + res.mNumSamples = 1325952; + res.mNumTrailingFrames = 2; + res.mBitrate = 256000; + res.mSlotSize = 1; + res.mPrivate = 0; + const int syncs[] = {2151, 2987, 3823, 4659, 5495, 6331}; + res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 6); + + // No content length can be estimated for CBR stream resources. + MP3Resource streamRes = res; + streamRes.mFileSize = -1; + streamRes.mDuration = Nothing(); + + res.mResource = new MockMP3MediaResource(res.mFilePath); + res.mDemuxer = new MP3TrackDemuxer(res.mResource); + mTargets.push_back(res); + + streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); + streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); + mTargets.push_back(streamRes); + } + + { + MP3Resource res; + // This file trips up the MP3 demuxer if ID3v2 tags aren't properly + // skipped. If skipping is not properly implemented, depending on the + // strictness of the MPEG frame parser a false sync will be detected + // somewhere within the metadata at or after 112087, or failing that, at + // the artificially added extraneous header at 114532. + res.mFilePath = "id3v2header.mp3"; + res.mIsVBR = false; + res.mHeaderType = MP3Resource::HeaderType::NONE; + res.mFileSize = 191302; + res.mMPEGLayer = 3; + res.mMPEGVersion = 1; + res.mID3MajorVersion = 3; + res.mID3MinorVersion = 0; + res.mID3Flags = 0; + res.mID3Size = 115304; + res.mDuration = Some(MP3Resource::Duration{3166167, 0.001f}); + res.mSeekError = 0.02f; + res.mSampleRate = 44100; + res.mSamplesPerFrame = 1152; + res.mNumSamples = 139392; + res.mNumTrailingFrames = 0; + res.mBitrate = 192000; + res.mSlotSize = 1; + res.mPrivate = 1; + const int syncs[] = {115314, 115941, 116568, 117195, 117822, 118449}; + res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 6); + + // No content length can be estimated for CBR stream resources. + MP3Resource streamRes = res; + streamRes.mFileSize = -1; + streamRes.mDuration = Nothing(); + + res.mResource = new MockMP3MediaResource(res.mFilePath); + res.mDemuxer = new MP3TrackDemuxer(res.mResource); + mTargets.push_back(res); + + streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); + streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); + mTargets.push_back(streamRes); + } + + { + MP3Resource res; + res.mFilePath = "noise_vbr.mp3"; + res.mIsVBR = true; + res.mHeaderType = MP3Resource::HeaderType::XING; + res.mFileSize = 583679; + res.mMPEGLayer = 3; + res.mMPEGVersion = 1; + res.mID3MajorVersion = 3; + res.mID3MinorVersion = 0; + res.mID3Flags = 0; + res.mID3Size = 2221; + res.mDuration = Some(MP3Resource::Duration{30081000, 0.005f}); + res.mSeekError = 0.02f; + res.mSampleRate = 44100; + res.mSamplesPerFrame = 1152; + res.mNumSamples = 1326575; + res.mNumTrailingFrames = 3; + res.mBitrate = 154000; + res.mSlotSize = 1; + res.mPrivate = 0; + const int syncs[] = {2231, 2648, 2752, 3796, 4318, 4735}; + res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 6); + + // VBR stream resources contain header info on total frames numbers, which + // is used to estimate the total duration. + MP3Resource streamRes = res; + streamRes.mFileSize = -1; + + res.mResource = new MockMP3MediaResource(res.mFilePath); + res.mDemuxer = new MP3TrackDemuxer(res.mResource); + mTargets.push_back(res); + + streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); + streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); + mTargets.push_back(streamRes); + } + + { + MP3Resource res; + res.mFilePath = "small-shot.mp3"; + res.mIsVBR = true; + res.mHeaderType = MP3Resource::HeaderType::XING; + res.mFileSize = 6825; + res.mMPEGLayer = 3; + res.mMPEGVersion = 1; + res.mID3MajorVersion = 4; + res.mID3MinorVersion = 0; + res.mID3Flags = 0; + res.mID3Size = 24; + res.mDuration = Some(MP3Resource::Duration{336686, 0.01f}); + res.mSeekError = 0.2f; + res.mSampleRate = 44100; + res.mSamplesPerFrame = 1152; + res.mNumSamples = 12; + res.mNumTrailingFrames = 0; + res.mBitrate = 256000; + res.mSlotSize = 1; + res.mPrivate = 0; + const int syncs[] = {34, 556, 1078, 1601, 2123, 2646, 3168, + 3691, 4213, 4736, 5258, 5781, 6303}; + res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 13); + + // No content length can be estimated for CBR stream resources. + MP3Resource streamRes = res; + streamRes.mFileSize = -1; + + res.mResource = new MockMP3MediaResource(res.mFilePath); + res.mDemuxer = new MP3TrackDemuxer(res.mResource); + mTargets.push_back(res); + + streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); + streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); + mTargets.push_back(streamRes); + } + + { + MP3Resource res; + // This file contains a false frame sync at 34, just after the ID3 tag, + // which should be identified as a false positive and skipped. + res.mFilePath = "small-shot-false-positive.mp3"; + res.mIsVBR = true; + res.mHeaderType = MP3Resource::HeaderType::XING; + res.mFileSize = 6845; + res.mMPEGLayer = 3; + res.mMPEGVersion = 1; + res.mID3MajorVersion = 4; + res.mID3MinorVersion = 0; + res.mID3Flags = 0; + res.mID3Size = 24; + res.mDuration = Some(MP3Resource::Duration{336686, 0.01f}); + res.mSeekError = 0.2f; + res.mSampleRate = 44100; + res.mSamplesPerFrame = 1152; + res.mNumSamples = 12; + res.mNumTrailingFrames = 0; + res.mBitrate = 256000; + res.mSlotSize = 1; + res.mPrivate = 0; + const int syncs[] = {54, 576, 1098, 1621, 2143, 2666, 3188, + 3711, 4233, 4756, 5278, 5801, 6323}; + res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 13); + + // No content length can be estimated for CBR stream resources. + MP3Resource streamRes = res; + streamRes.mFileSize = -1; + + res.mResource = new MockMP3MediaResource(res.mFilePath); + res.mDemuxer = new MP3TrackDemuxer(res.mResource); + mTargets.push_back(res); + + streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); + streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); + mTargets.push_back(streamRes); + } + + { + MP3Resource res; + res.mFilePath = "small-shot-partial-xing.mp3"; + res.mIsVBR = true; + res.mHeaderType = MP3Resource::HeaderType::XING; + res.mFileSize = 6825; + res.mMPEGLayer = 3; + res.mMPEGVersion = 1; + res.mID3MajorVersion = 4; + res.mID3MinorVersion = 0; + res.mID3Flags = 0; + res.mID3Size = 24; + res.mDuration = Some(MP3Resource::Duration{336686, 0.01f}); + res.mSeekError = 0.2f; + res.mSampleRate = 44100; + res.mSamplesPerFrame = 1152; + res.mNumSamples = 12; + res.mNumTrailingFrames = 0; + res.mBitrate = 256000; + res.mSlotSize = 1; + res.mPrivate = 0; + const int syncs[] = {34, 556, 1078, 1601, 2123, 2646, 3168, + 3691, 4213, 4736, 5258, 5781, 6303}; + res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 13); + + // No content length can be estimated for CBR stream resources. + MP3Resource streamRes = res; + streamRes.mFileSize = -1; + + res.mResource = new MockMP3MediaResource(res.mFilePath); + res.mDemuxer = new MP3TrackDemuxer(res.mResource); + mTargets.push_back(res); + + streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); + streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); + mTargets.push_back(streamRes); + } + + { + MP3Resource res; + res.mFilePath = "test_vbri.mp3"; + res.mIsVBR = true; + res.mHeaderType = MP3Resource::HeaderType::VBRI; + res.mFileSize = 16519; + res.mMPEGLayer = 3; + res.mMPEGVersion = 1; + res.mID3MajorVersion = 3; + res.mID3MinorVersion = 0; + res.mID3Flags = 0; + res.mID3Size = 4202; + res.mDuration = Some(MP3Resource::Duration{783660, 0.01f}); + res.mSeekError = 0.02f; + res.mSampleRate = 44100; + res.mSamplesPerFrame = 1152; + res.mNumSamples = 29; + res.mNumTrailingFrames = 0; + res.mBitrate = 0; + res.mSlotSize = 1; + res.mPrivate = 0; + const int syncs[] = {4212, 4734, 5047, 5464, 5986, 6403}; + res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 6); + + // VBR stream resources contain header info on total frames numbers, which + // is used to estimate the total duration. + MP3Resource streamRes = res; + streamRes.mFileSize = -1; + + res.mResource = new MockMP3MediaResource(res.mFilePath); + res.mDemuxer = new MP3TrackDemuxer(res.mResource); + mTargets.push_back(res); + + streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); + streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); + mTargets.push_back(streamRes); + } + + for (auto& target : mTargets) { + ASSERT_EQ(NS_OK, target.mResource->Open()); + ASSERT_TRUE(target.mDemuxer->Init()); + } + } + + std::vector<MP3Resource> mTargets; +}; + +TEST_F(MP3DemuxerTest, ID3Tags) { + for (const auto& target : mTargets) { + RefPtr<MediaRawData> frame(target.mDemuxer->DemuxSample()); + ASSERT_TRUE(frame); + + const auto& id3 = target.mDemuxer->ID3Header(); + ASSERT_TRUE(id3.IsValid()); + + EXPECT_EQ(target.mID3MajorVersion, id3.MajorVersion()); + EXPECT_EQ(target.mID3MinorVersion, id3.MinorVersion()); + EXPECT_EQ(target.mID3Flags, id3.Flags()); + EXPECT_EQ(target.mID3Size, id3.Size()); + } +} + +TEST_F(MP3DemuxerTest, VBRHeader) { + for (const auto& target : mTargets) { + RefPtr<MediaRawData> frame(target.mDemuxer->DemuxSample()); + ASSERT_TRUE(frame); + + const auto& vbr = target.mDemuxer->VBRInfo(); + + if (target.mHeaderType == MP3Resource::HeaderType::XING) { + EXPECT_EQ(FrameParser::VBRHeader::XING, vbr.Type()); + // TODO: find reference number which accounts for trailing headers. + // EXPECT_EQ(target.mNumSamples / target.mSamplesPerFrame, + // vbr.NumAudioFrames().value()); + } else if (target.mHeaderType == MP3Resource::HeaderType::VBRI) { + EXPECT_TRUE(target.mIsVBR); + EXPECT_EQ(FrameParser::VBRHeader::VBRI, vbr.Type()); + } else { // MP3Resource::HeaderType::NONE + EXPECT_EQ(FrameParser::VBRHeader::NONE, vbr.Type()); + EXPECT_FALSE(vbr.NumAudioFrames()); + } + } +} + +TEST_F(MP3DemuxerTest, FrameParsing) { + for (const auto& target : mTargets) { + RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample()); + ASSERT_TRUE(frameData); + EXPECT_EQ(target.mFileSize, target.mDemuxer->StreamLength()); + + const auto& id3 = target.mDemuxer->ID3Header(); + ASSERT_TRUE(id3.IsValid()); + + int64_t parsedLength = id3.Size(); + int64_t bitrateSum = 0; + int32_t numFrames = 0; + int32_t numSamples = 0; + + while (frameData) { + if (static_cast<int64_t>(target.mSyncOffsets.size()) > numFrames) { + // Test sync offsets. + EXPECT_EQ(target.mSyncOffsets[numFrames], frameData->mOffset); + } + + ++numFrames; + parsedLength += frameData->Size(); + + const auto& frame = target.mDemuxer->LastFrame(); + const auto& header = frame.Header(); + ASSERT_TRUE(header.IsValid()); + + numSamples += header.SamplesPerFrame(); + + EXPECT_EQ(target.mMPEGLayer, header.Layer()); + EXPECT_EQ(target.mSampleRate, header.SampleRate()); + EXPECT_EQ(target.mSamplesPerFrame, header.SamplesPerFrame()); + EXPECT_EQ(target.mSlotSize, header.SlotSize()); + EXPECT_EQ(target.mPrivate, header.Private()); + + if (target.mIsVBR) { + // Used to compute the average bitrate for VBR streams. + bitrateSum += target.mBitrate; + } else { + EXPECT_EQ(target.mBitrate, header.Bitrate()); + } + + frameData = target.mDemuxer->DemuxSample(); + } + + // TODO: find reference number which accounts for trailing headers. + // EXPECT_EQ(target.mNumSamples / target.mSamplesPerFrame, numFrames); + // EXPECT_EQ(target.mNumSamples, numSamples); + + // There may be trailing headers which we don't parse, so the stream length + // is the upper bound. + if (target.mFileSize > 0) { + EXPECT_GE(target.mFileSize, parsedLength); + } + + if (target.mIsVBR) { + ASSERT_TRUE(numFrames); + EXPECT_EQ(target.mBitrate, static_cast<int32_t>(bitrateSum / numFrames)); + } + } +} + +TEST_F(MP3DemuxerTest, Duration) { + for (const auto& target : mTargets) { + RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample()); + ASSERT_TRUE(frameData); + EXPECT_EQ(target.mFileSize, target.mDemuxer->StreamLength()); + + while (frameData) { + if (target.mDuration) { + ASSERT_TRUE(target.mDemuxer->Duration()); + EXPECT_NEAR(target.mDuration->mMicroseconds, + target.mDemuxer->Duration()->ToMicroseconds(), + target.mDuration->Tolerance()); + } else { + EXPECT_FALSE(target.mDemuxer->Duration()); + } + frameData = target.mDemuxer->DemuxSample(); + } + } + + // Seek out of range tests. + for (const auto& target : mTargets) { + // Skip tests for stream media resources because of lacking duration. + if (target.mFileSize <= 0) { + continue; + } + + target.mDemuxer->Reset(); + RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample()); + ASSERT_TRUE(frameData); + + ASSERT_TRUE(target.mDemuxer->Duration()); + const auto duration = target.mDemuxer->Duration().value(); + const auto pos = duration + TimeUnit::FromMicroseconds(1e6); + + // Attempt to seek 1 second past the end of stream. + target.mDemuxer->Seek(pos); + // The seek should bring us to the end of the stream. + EXPECT_NEAR(duration.ToMicroseconds(), + target.mDemuxer->SeekPosition().ToMicroseconds(), + target.mSeekError * duration.ToMicroseconds()); + + // Since we're at the end of the stream, there should be no frames left. + frameData = target.mDemuxer->DemuxSample(); + ASSERT_FALSE(frameData); + } +} + +TEST_F(MP3DemuxerTest, Seek) { + for (const auto& target : mTargets) { + RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample()); + ASSERT_TRUE(frameData); + + const auto seekTime = TimeUnit::FromSeconds(1); + auto pos = target.mDemuxer->SeekPosition(); + + while (frameData) { + EXPECT_NEAR(pos.ToMicroseconds(), + target.mDemuxer->SeekPosition().ToMicroseconds(), + target.mSeekError * pos.ToMicroseconds()); + + pos += seekTime; + target.mDemuxer->Seek(pos); + frameData = target.mDemuxer->DemuxSample(); + } + } + + // Seeking should work with in-between resets, too. + for (const auto& target : mTargets) { + target.mDemuxer->Reset(); + RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample()); + ASSERT_TRUE(frameData); + + const auto seekTime = TimeUnit::FromSeconds(1); + auto pos = target.mDemuxer->SeekPosition(); + + while (frameData) { + EXPECT_NEAR(pos.ToMicroseconds(), + target.mDemuxer->SeekPosition().ToMicroseconds(), + target.mSeekError * pos.ToMicroseconds()); + + pos += seekTime; + target.mDemuxer->Reset(); + target.mDemuxer->Seek(pos); + frameData = target.mDemuxer->DemuxSample(); + } + } +} diff --git a/dom/media/gtest/TestMP4Demuxer.cpp b/dom/media/gtest/TestMP4Demuxer.cpp new file mode 100644 index 0000000000..536860ef49 --- /dev/null +++ b/dom/media/gtest/TestMP4Demuxer.cpp @@ -0,0 +1,613 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "MP4Demuxer.h" +#include "mozilla/MozPromise.h" +#include "MediaDataDemuxer.h" +#include "mozilla/SharedThreadPool.h" +#include "mozilla/TaskQueue.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Unused.h" +#include "MockMediaResource.h" +#include "VideoUtils.h" + +using namespace mozilla; +using media::TimeUnit; + +#define DO_FAIL \ + [binding]() -> void { \ + EXPECT_TRUE(false); \ + binding->mTaskQueue->BeginShutdown(); \ + } + +class MP4DemuxerBinding { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MP4DemuxerBinding); + + RefPtr<MockMediaResource> resource; + RefPtr<MP4Demuxer> mDemuxer; + RefPtr<TaskQueue> mTaskQueue; + RefPtr<MediaTrackDemuxer> mAudioTrack; + RefPtr<MediaTrackDemuxer> mVideoTrack; + uint32_t mIndex; + nsTArray<RefPtr<MediaRawData>> mSamples; + nsTArray<int64_t> mKeyFrameTimecodes; + MozPromiseHolder<GenericPromise> mCheckTrackKeyFramePromise; + MozPromiseHolder<GenericPromise> mCheckTrackSamples; + + explicit MP4DemuxerBinding(const char* aFileName = "dash_dashinit.mp4") + : resource(new MockMediaResource(aFileName)), + mDemuxer(new MP4Demuxer(resource)), + mTaskQueue( + new TaskQueue(GetMediaThreadPool(MediaThreadType::SUPERVISOR))), + mIndex(0) { + EXPECT_EQ(NS_OK, resource->Open()); + } + + template <typename Function> + void RunTestAndWait(const Function& aFunction) { + Function func(aFunction); + RefPtr<MP4DemuxerBinding> binding = this; + mDemuxer->Init()->Then(mTaskQueue, __func__, std::move(func), DO_FAIL); + mTaskQueue->AwaitShutdownAndIdle(); + } + + RefPtr<GenericPromise> CheckTrackKeyFrame(MediaTrackDemuxer* aTrackDemuxer) { + MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn()); + + RefPtr<MediaTrackDemuxer> track = aTrackDemuxer; + RefPtr<MP4DemuxerBinding> binding = this; + + auto time = TimeUnit::Invalid(); + while (mIndex < mSamples.Length()) { + uint32_t i = mIndex++; + if (mSamples[i]->mKeyframe) { + time = mSamples[i]->mTime; + break; + } + } + + RefPtr<GenericPromise> p = mCheckTrackKeyFramePromise.Ensure(__func__); + + if (!time.IsValid()) { + mCheckTrackKeyFramePromise.Resolve(true, __func__); + return p; + } + + DispatchTask([track, time, binding]() { + track->Seek(time)->Then( + binding->mTaskQueue, __func__, + [track, time, binding]() { + track->GetSamples()->Then( + binding->mTaskQueue, __func__, + [track, time, + binding](RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples) { + EXPECT_EQ(time, aSamples->GetSamples()[0]->mTime); + binding->CheckTrackKeyFrame(track); + }, + DO_FAIL); + }, + DO_FAIL); + }); + + return p; + } + + RefPtr<GenericPromise> CheckTrackSamples(MediaTrackDemuxer* aTrackDemuxer) { + MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn()); + + RefPtr<MediaTrackDemuxer> track = aTrackDemuxer; + RefPtr<MP4DemuxerBinding> binding = this; + + RefPtr<GenericPromise> p = mCheckTrackSamples.Ensure(__func__); + + DispatchTask([track, binding]() { + track->GetSamples()->Then( + binding->mTaskQueue, __func__, + [track, binding](RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples) { + if (aSamples->GetSamples().Length()) { + binding->mSamples.AppendElements(aSamples->GetSamples()); + binding->CheckTrackSamples(track); + } + }, + [binding](const MediaResult& aError) { + if (aError == NS_ERROR_DOM_MEDIA_END_OF_STREAM) { + EXPECT_TRUE(binding->mSamples.Length() > 1); + for (uint32_t i = 0; i < (binding->mSamples.Length() - 1); i++) { + EXPECT_LT(binding->mSamples[i]->mTimecode, + binding->mSamples[i + 1]->mTimecode); + if (binding->mSamples[i]->mKeyframe) { + binding->mKeyFrameTimecodes.AppendElement( + binding->mSamples[i]->mTimecode.ToMicroseconds()); + } + } + binding->mCheckTrackSamples.Resolve(true, __func__); + } else { + EXPECT_TRUE(false); + binding->mCheckTrackSamples.Reject(aError, __func__); + } + }); + }); + + return p; + } + + private: + template <typename FunctionType> + void DispatchTask(FunctionType aFun) { + RefPtr<Runnable> r = + NS_NewRunnableFunction("MP4DemuxerBinding::DispatchTask", aFun); + Unused << mTaskQueue->Dispatch(r.forget()); + } + + virtual ~MP4DemuxerBinding() = default; +}; + +TEST(MP4Demuxer, Seek) +{ + RefPtr<MP4DemuxerBinding> binding = new MP4DemuxerBinding(); + + binding->RunTestAndWait([binding]() { + binding->mVideoTrack = + binding->mDemuxer->GetTrackDemuxer(TrackInfo::kVideoTrack, 0); + binding->CheckTrackSamples(binding->mVideoTrack) + ->Then( + binding->mTaskQueue, __func__, + [binding]() { + binding->CheckTrackKeyFrame(binding->mVideoTrack) + ->Then( + binding->mTaskQueue, __func__, + [binding]() { binding->mTaskQueue->BeginShutdown(); }, + DO_FAIL); + }, + DO_FAIL); + }); +} + +static nsCString ToCryptoString(const CryptoSample& aCrypto) { + nsCString res; + if (aCrypto.IsEncrypted()) { + res.AppendPrintf("%d ", aCrypto.mIVSize); + for (size_t i = 0; i < aCrypto.mKeyId.Length(); i++) { + res.AppendPrintf("%02x", aCrypto.mKeyId[i]); + } + res.AppendLiteral(" "); + for (size_t i = 0; i < aCrypto.mIV.Length(); i++) { + res.AppendPrintf("%02x", aCrypto.mIV[i]); + } + EXPECT_EQ(aCrypto.mPlainSizes.Length(), aCrypto.mEncryptedSizes.Length()); + for (size_t i = 0; i < aCrypto.mPlainSizes.Length(); i++) { + res.AppendPrintf(" %d,%d", aCrypto.mPlainSizes[i], + aCrypto.mEncryptedSizes[i]); + } + } else { + res.AppendLiteral("no crypto"); + } + return res; +} + +TEST(MP4Demuxer, CENCFragVideo) +{ + const char* video[] = { + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000000 " + "5,684 5,16980", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000450 " + "5,1826", + "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000004c3 " + "5,1215", + "16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000050f " + "5,1302", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000561 " + "5,939", + "16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000059c " + "5,763", + "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000005cc " + "5,672", + "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000005f6 " + "5,748", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000625 " + "5,1025", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000666 " + "5,730", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000694 " + "5,897", + "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000006cd " + "5,643", + "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000006f6 " + "5,556", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000719 " + "5,527", + "16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000073a " + "5,606", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000760 " + "5,701", + "16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000078c " + "5,531", + "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000007ae " + "5,562", + "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000007d2 " + "5,576", + "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000007f6 " + "5,514", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000817 " + "5,404", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000831 " + "5,635", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000859 " + "5,433", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000875 " + "5,478", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000893 " + "5,474", + "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000008b1 " + "5,462", + "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000008ce " + "5,473", + "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000008ec " + "5,437", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000908 " + "5,418", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000923 " + "5,475", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000941 " + "5,23133", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000ee7 " + "5,475", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f05 " + "5,402", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f1f " + "5,415", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f39 " + "5,408", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f53 " + "5,442", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f6f " + "5,385", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f88 " + "5,368", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f9f " + "5,354", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000fb6 " + "5,400", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000fcf " + "5,399", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000fe8 " + "5,1098", + "16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000102d " + "5,1508", + "16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000108c " + "5,1345", + "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000010e1 " + "5,1945", + "16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000115b " + "5,1824", + "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000011cd " + "5,2133", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000001253 " + "5,2486", + "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000012ef " + "5,1739", + "16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000135c " + "5,1836", + "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000013cf " + "5,2367", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000001463 " + "5,2571", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000001504 " + "5,3008", + "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000015c0 " + "5,3255", + "16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000168c " + "5,3225", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000001756 " + "5,3118", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000001819 " + "5,2407", + "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000018b0 " + "5,2400", + "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000001946 " + "5,2158", + "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000019cd " + "5,2392", + }; + + RefPtr<MP4DemuxerBinding> binding = new MP4DemuxerBinding("gizmo-frag.mp4"); + + binding->RunTestAndWait([binding, video]() { + // grab all video samples. + binding->mVideoTrack = + binding->mDemuxer->GetTrackDemuxer(TrackInfo::kVideoTrack, 0); + binding->CheckTrackSamples(binding->mVideoTrack) + ->Then( + binding->mTaskQueue, __func__, + [binding, video]() { + for (uint32_t i = 0; i < binding->mSamples.Length(); i++) { + nsCString text = ToCryptoString(binding->mSamples[i]->mCrypto); + EXPECT_STREQ(video[i++], text.get()); + } + EXPECT_EQ(ArrayLength(video), binding->mSamples.Length()); + binding->mTaskQueue->BeginShutdown(); + }, + DO_FAIL); + }); +} + +TEST(MP4Demuxer, CENCFragAudio) +{ + const char* audio[] = { + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000000 " + "0,281", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000012 " + "0,257", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000023 " + "0,246", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000033 " + "0,257", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000044 " + "0,260", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000055 " + "0,260", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000066 " + "0,272", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000077 " + "0,280", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000089 " + "0,284", + "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000009b " + "0,290", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000000ae " + "0,278", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000000c0 " + "0,268", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000000d1 " + "0,307", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000000e5 " + "0,290", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000000f8 " + "0,304", + "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000010b " + "0,316", + "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000011f " + "0,308", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000133 " + "0,301", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000146 " + "0,318", + "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000015a " + "0,311", + "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000016e " + "0,303", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000181 " + "0,325", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000196 " + "0,334", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000001ab " + "0,344", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000001c1 " + "0,344", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000001d7 " + "0,387", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000001f0 " + "0,396", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000209 " + "0,368", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000220 " + "0,373", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000238 " + "0,425", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000253 " + "0,428", + "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000026e " + "0,426", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000289 " + "0,427", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000002a4 " + "0,424", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000002bf " + "0,447", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000002db " + "0,446", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000002f7 " + "0,442", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000313 " + "0,444", + "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000032f " + "0,374", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000347 " + "0,405", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000361 " + "0,372", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000379 " + "0,395", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000392 " + "0,435", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000003ae " + "0,426", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000003c9 " + "0,430", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000003e4 " + "0,390", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000003fd " + "0,335", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000412 " + "0,339", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000428 " + "0,352", + "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000043e " + "0,364", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000455 " + "0,398", + "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000046e " + "0,451", + "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000048b " + "0,448", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000004a7 " + "0,436", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000004c3 " + "0,424", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000004de " + "0,428", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000004f9 " + "0,413", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000513 " + "0,430", + "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000052e " + "0,450", + "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000054b " + "0,386", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000564 " + "0,320", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000578 " + "0,347", + "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000058e " + "0,382", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000005a6 " + "0,437", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000005c2 " + "0,387", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000005db " + "0,340", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000005f1 " + "0,337", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000607 " + "0,389", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000620 " + "0,428", + "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000063b " + "0,426", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000656 " + "0,446", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000672 " + "0,456", + "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000068f " + "0,468", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000006ad " + "0,468", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000006cb " + "0,463", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000006e8 " + "0,467", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000706 " + "0,460", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000723 " + "0,446", + "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000073f " + "0,453", + "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000075c " + "0,448", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000778 " + "0,446", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000794 " + "0,439", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000007b0 " + "0,436", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000007cc " + "0,441", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000007e8 " + "0,465", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000806 " + "0,448", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000822 " + "0,448", + "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000083e " + "0,469", + "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000085c " + "0,431", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000877 " + "0,437", + "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000893 " + "0,474", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000008b1 " + "0,436", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000008cd " + "0,433", + "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000008e9 " + "0,481", + }; + + RefPtr<MP4DemuxerBinding> binding = new MP4DemuxerBinding("gizmo-frag.mp4"); + + binding->RunTestAndWait([binding, audio]() { + // grab all audio samples. + binding->mAudioTrack = + binding->mDemuxer->GetTrackDemuxer(TrackInfo::kAudioTrack, 0); + binding->CheckTrackSamples(binding->mAudioTrack) + ->Then( + binding->mTaskQueue, __func__, + [binding, audio]() { + EXPECT_TRUE(binding->mSamples.Length() > 1); + for (uint32_t i = 0; i < binding->mSamples.Length(); i++) { + nsCString text = ToCryptoString(binding->mSamples[i]->mCrypto); + EXPECT_STREQ(audio[i++], text.get()); + } + EXPECT_EQ(ArrayLength(audio), binding->mSamples.Length()); + binding->mTaskQueue->BeginShutdown(); + }, + DO_FAIL); + }); +} + +TEST(MP4Demuxer, GetNextKeyframe) +{ + RefPtr<MP4DemuxerBinding> binding = new MP4DemuxerBinding("gizmo-frag.mp4"); + + binding->RunTestAndWait([binding]() { + // Insert a [0,end] buffered range, to simulate Moof's being buffered + // via MSE. + auto len = binding->resource->GetLength(); + binding->resource->MockAddBufferedRange(0, len); + + // gizmp-frag has two keyframes; one at dts=cts=0, and another at + // dts=cts=1000000. Verify we get expected results. + TimeUnit time; + binding->mVideoTrack = + binding->mDemuxer->GetTrackDemuxer(TrackInfo::kVideoTrack, 0); + binding->mVideoTrack->Reset(); + binding->mVideoTrack->GetNextRandomAccessPoint(&time); + EXPECT_EQ(time.ToMicroseconds(), 0); + binding->mVideoTrack->GetSamples()->Then( + binding->mTaskQueue, __func__, + [binding]() { + TimeUnit time; + binding->mVideoTrack->GetNextRandomAccessPoint(&time); + EXPECT_EQ(time.ToMicroseconds(), 1000000); + binding->mTaskQueue->BeginShutdown(); + }, + DO_FAIL); + }); +} + +TEST(MP4Demuxer, ZeroInLastMoov) +{ + RefPtr<MP4DemuxerBinding> binding = + new MP4DemuxerBinding("short-zero-in-moov.mp4"); + binding->RunTestAndWait([binding]() { + // It demuxes without error. That is sufficient. + binding->mTaskQueue->BeginShutdown(); + }); +} + +TEST(MP4Demuxer, ZeroInMoovQuickTime) +{ + RefPtr<MP4DemuxerBinding> binding = + new MP4DemuxerBinding("short-zero-inband.mov"); + binding->RunTestAndWait([binding]() { + // It demuxes without error. That is sufficient. + binding->mTaskQueue->BeginShutdown(); + }); +} + +TEST(MP4Demuxer, IgnoreMinus1Duration) +{ + RefPtr<MP4DemuxerBinding> binding = + new MP4DemuxerBinding("negative_duration.mp4"); + binding->RunTestAndWait([binding]() { + // It demuxes without error. That is sufficient. + binding->mTaskQueue->BeginShutdown(); + }); +} + +#undef DO_FAIL diff --git a/dom/media/gtest/TestMediaDataDecoder.cpp b/dom/media/gtest/TestMediaDataDecoder.cpp new file mode 100644 index 0000000000..f25ad048ee --- /dev/null +++ b/dom/media/gtest/TestMediaDataDecoder.cpp @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "Benchmark.h" +#include "MockMediaResource.h" +#include "DecoderTraits.h" +#include "MediaContainerType.h" +#include "MP4Demuxer.h" +#include "WebMDecoder.h" +#include "WebMDemuxer.h" +#include "mozilla/AbstractThread.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "nsMimeTypes.h" + +using namespace mozilla; + +class BenchmarkRunner { + public: + explicit BenchmarkRunner(Benchmark* aBenchmark) : mBenchmark(aBenchmark) {} + + uint32_t Run() { + bool done = false; + uint32_t result = 0; + + mBenchmark->Init(); + mBenchmark->Run()->Then( + // Non DocGroup-version of AbstractThread::MainThread() is fine for + // testing. + AbstractThread::MainThread(), __func__, + [&](uint32_t aDecodeFps) { + result = aDecodeFps; + done = true; + }, + [&]() { done = true; }); + + // Wait until benchmark completes. + SpinEventLoopUntil([&]() { return done; }); + return result; + } + + private: + RefPtr<Benchmark> mBenchmark; +}; + +TEST(MediaDataDecoder, H264) +{ + if (!DecoderTraits::IsMP4SupportedType( + MediaContainerType(MEDIAMIMETYPE(VIDEO_MP4)), + /* DecoderDoctorDiagnostics* */ nullptr)) { + EXPECT_TRUE(true); + } else { + RefPtr<MockMediaResource> resource = new MockMediaResource("gizmo.mp4"); + nsresult rv = resource->Open(); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + + BenchmarkRunner runner(new Benchmark(new MP4Demuxer(resource))); + EXPECT_GT(runner.Run(), 0u); + } +} + +TEST(MediaDataDecoder, VP9) +{ + if (!WebMDecoder::IsSupportedType( + MediaContainerType(MEDIAMIMETYPE(VIDEO_WEBM)))) { + EXPECT_TRUE(true); + } else { + RefPtr<MockMediaResource> resource = new MockMediaResource("vp9cake.webm"); + nsresult rv = resource->Open(); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + + BenchmarkRunner runner(new Benchmark(new WebMDemuxer(resource))); + EXPECT_GT(runner.Run(), 0u); + } +} diff --git a/dom/media/gtest/TestMediaDataEncoder.cpp b/dom/media/gtest/TestMediaDataEncoder.cpp new file mode 100644 index 0000000000..6adeb73f3d --- /dev/null +++ b/dom/media/gtest/TestMediaDataEncoder.cpp @@ -0,0 +1,362 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "AnnexB.h" +#include "ImageContainer.h" +#include "mozilla/AbstractThread.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/media/MediaUtils.h" // For media::Await +#include "nsMimeTypes.h" +#include "PEMFactory.h" +#include "TimeUnits.h" +#include "VideoUtils.h" +#include <algorithm> + +#include <fstream> + +#define SKIP_IF_NOT_SUPPORTED(mimeType) \ + do { \ + RefPtr<PEMFactory> f(new PEMFactory()); \ + if (!f->SupportsMimeType(nsLiteralCString(mimeType))) { \ + return; \ + } \ + } while (0) + +#define BLOCK_SIZE 64 +#define WIDTH 640 +#define HEIGHT 480 +#define NUM_FRAMES 150UL +#define FRAME_RATE 30 +#define FRAME_DURATION (1000000 / FRAME_RATE) +#define BIT_RATE (1000 * 1000) // 1Mbps +#define KEYFRAME_INTERVAL FRAME_RATE // 1 keyframe per second + +using namespace mozilla; + +static gfx::IntSize kImageSize(WIDTH, HEIGHT); + +class MediaDataEncoderTest : public testing::Test { + protected: + void SetUp() override { mData.Init(kImageSize); } + + void TearDown() override { mData.Deinit(); } + + public: + struct FrameSource final { + layers::PlanarYCbCrData mYUV; + UniquePtr<uint8_t[]> mBuffer; + RefPtr<layers::BufferRecycleBin> mRecycleBin; + int16_t mColorStep = 4; + + void Init(const gfx::IntSize& aSize) { + mYUV.mPicSize = aSize; + mYUV.mYStride = aSize.width; + mYUV.mYSize = aSize; + mYUV.mCbCrStride = aSize.width / 2; + mYUV.mCbCrSize = gfx::IntSize(aSize.width / 2, aSize.height / 2); + size_t bufferSize = mYUV.mYStride * mYUV.mYSize.height + + mYUV.mCbCrStride * mYUV.mCbCrSize.height + + mYUV.mCbCrStride * mYUV.mCbCrSize.height; + mBuffer = MakeUnique<uint8_t[]>(bufferSize); + std::fill_n(mBuffer.get(), bufferSize, 0x7F); + mYUV.mYChannel = mBuffer.get(); + mYUV.mCbChannel = mYUV.mYChannel + mYUV.mYStride * mYUV.mYSize.height; + mYUV.mCrChannel = + mYUV.mCbChannel + mYUV.mCbCrStride * mYUV.mCbCrSize.height; + mRecycleBin = new layers::BufferRecycleBin(); + } + + void Deinit() { + mBuffer.reset(); + mRecycleBin = nullptr; + } + + already_AddRefed<MediaData> GetFrame(const size_t aIndex) { + Draw(aIndex); + RefPtr<layers::PlanarYCbCrImage> img = + new layers::RecyclingPlanarYCbCrImage(mRecycleBin); + img->CopyData(mYUV); + RefPtr<MediaData> frame = VideoData::CreateFromImage( + kImageSize, 0, + media::TimeUnit::FromMicroseconds(aIndex * FRAME_DURATION), + media::TimeUnit::FromMicroseconds(FRAME_DURATION), img, + (aIndex & 0xF) == 0, + media::TimeUnit::FromMicroseconds(aIndex * FRAME_DURATION)); + return frame.forget(); + } + + void DrawChessboard(uint8_t* aAddr, const size_t aWidth, + const size_t aHeight, const size_t aOffset) { + uint8_t pixels[2][BLOCK_SIZE]; + size_t x = aOffset % BLOCK_SIZE; + if ((aOffset / BLOCK_SIZE) & 1) { + x = BLOCK_SIZE - x; + } + for (size_t i = 0; i < x; i++) { + pixels[0][i] = 0x00; + pixels[1][i] = 0xFF; + } + for (size_t i = x; i < BLOCK_SIZE; i++) { + pixels[0][i] = 0xFF; + pixels[1][i] = 0x00; + } + + uint8_t* p = aAddr; + for (size_t row = 0; row < aHeight; row++) { + for (size_t col = 0; col < aWidth; col += BLOCK_SIZE) { + memcpy(p, pixels[((row / BLOCK_SIZE) + (col / BLOCK_SIZE)) % 2], + BLOCK_SIZE); + p += BLOCK_SIZE; + } + } + } + + void Draw(const size_t aIndex) { + DrawChessboard(mYUV.mYChannel, mYUV.mYSize.width, mYUV.mYSize.height, + aIndex << 1); + int16_t color = mYUV.mCbChannel[0] + mColorStep; + if (color > 255 || color < 0) { + mColorStep = -mColorStep; + color = mYUV.mCbChannel[0] + mColorStep; + } + + size_t size = (mYUV.mCrChannel - mYUV.mCbChannel); + + std::fill_n(mYUV.mCbChannel, size, static_cast<uint8_t>(color)); + std::fill_n(mYUV.mCrChannel, size, 0xFF - static_cast<uint8_t>(color)); + } + }; + + public: + FrameSource mData; +}; + +static already_AddRefed<MediaDataEncoder> CreateH264Encoder( + MediaDataEncoder::Usage aUsage = MediaDataEncoder::Usage::Realtime, + MediaDataEncoder::PixelFormat aPixelFormat = + MediaDataEncoder::PixelFormat::YUV420P, + int32_t aWidth = WIDTH, int32_t aHeight = HEIGHT, + const Maybe<MediaDataEncoder::H264Specific>& aSpecific = + Some(MediaDataEncoder::H264Specific( + KEYFRAME_INTERVAL, + MediaDataEncoder::H264Specific::ProfileLevel::BaselineAutoLevel))) { + RefPtr<PEMFactory> f(new PEMFactory()); + + if (!f->SupportsMimeType(nsLiteralCString(VIDEO_MP4))) { + return nullptr; + } + + VideoInfo videoInfo(aWidth, aHeight); + videoInfo.mMimeType = nsLiteralCString(VIDEO_MP4); + const RefPtr<TaskQueue> taskQueue( + new TaskQueue(GetMediaThreadPool(MediaThreadType::PLATFORM_ENCODER))); + + RefPtr<MediaDataEncoder> e; + if (aSpecific) { + e = f->CreateEncoder(CreateEncoderParams( + videoInfo /* track info */, aUsage, taskQueue, aPixelFormat, + FRAME_RATE /* FPS */, BIT_RATE /* bitrate */, aSpecific.value())); + } else { + e = f->CreateEncoder(CreateEncoderParams( + videoInfo /* track info */, aUsage, taskQueue, aPixelFormat, + FRAME_RATE /* FPS */, BIT_RATE /* bitrate */)); + } + + return e.forget(); +} + +void WaitForShutdown(RefPtr<MediaDataEncoder> aEncoder) { + MOZ_ASSERT(aEncoder); + + Maybe<bool> result; + // media::Await() supports exclusive promises only, but ShutdownPromise is + // not. + aEncoder->Shutdown()->Then( + AbstractThread::MainThread(), __func__, + [&result](bool rv) { + EXPECT_TRUE(rv); + result = Some(true); + }, + [&result]() { + FAIL() << "Shutdown should never be rejected"; + result = Some(false); + }); + SpinEventLoopUntil([&result]() { return result; }); +} + +TEST_F(MediaDataEncoderTest, H264Create) { + SKIP_IF_NOT_SUPPORTED(VIDEO_MP4); + + RefPtr<MediaDataEncoder> e = CreateH264Encoder(); + + EXPECT_TRUE(e); + + WaitForShutdown(e); +} + +static bool EnsureInit(RefPtr<MediaDataEncoder> aEncoder) { + if (!aEncoder) { + return false; + } + + bool succeeded; + media::Await( + GetMediaThreadPool(MediaThreadType::SUPERVISOR), aEncoder->Init(), + [&succeeded](TrackInfo::TrackType t) { + EXPECT_EQ(TrackInfo::TrackType::kVideoTrack, t); + succeeded = true; + }, + [&succeeded](MediaResult r) { succeeded = false; }); + return succeeded; +} + +TEST_F(MediaDataEncoderTest, H264InitWithoutSpecific) { + SKIP_IF_NOT_SUPPORTED(VIDEO_MP4); + + RefPtr<MediaDataEncoder> e = CreateH264Encoder( + MediaDataEncoder::Usage::Realtime, MediaDataEncoder::PixelFormat::YUV420P, + WIDTH, HEIGHT, Nothing()); + +#if defined(MOZ_WIDGET_ANDROID) // Android encoder requires I-frame interval + EXPECT_FALSE(EnsureInit(e)); +#else + EXPECT_TRUE(EnsureInit(e)); +#endif + + WaitForShutdown(e); +} + +TEST_F(MediaDataEncoderTest, H264Init) { + SKIP_IF_NOT_SUPPORTED(VIDEO_MP4); + + RefPtr<MediaDataEncoder> e = CreateH264Encoder(); + + EXPECT_TRUE(EnsureInit(e)); + + WaitForShutdown(e); +} + +static MediaDataEncoder::EncodedData Encode( + const RefPtr<MediaDataEncoder> aEncoder, const size_t aNumFrames, + MediaDataEncoderTest::FrameSource& aSource) { + MediaDataEncoder::EncodedData output; + bool succeeded; + for (size_t i = 0; i < aNumFrames; i++) { + RefPtr<MediaData> frame = aSource.GetFrame(i); + media::Await( + GetMediaThreadPool(MediaThreadType::SUPERVISOR), + aEncoder->Encode(frame), + [&output, &succeeded](MediaDataEncoder::EncodedData encoded) { + output.AppendElements(std::move(encoded)); + succeeded = true; + }, + [&succeeded](MediaResult r) { succeeded = false; }); + EXPECT_TRUE(succeeded); + if (!succeeded) { + return output; + } + } + + size_t pending = 0; + do { + media::Await( + GetMediaThreadPool(MediaThreadType::SUPERVISOR), aEncoder->Drain(), + [&pending, &output, &succeeded](MediaDataEncoder::EncodedData encoded) { + pending = encoded.Length(); + output.AppendElements(std::move(encoded)); + succeeded = true; + }, + [&succeeded](MediaResult r) { succeeded = false; }); + EXPECT_TRUE(succeeded); + if (!succeeded) { + return output; + } + } while (pending > 0); + + return output; +} + +TEST_F(MediaDataEncoderTest, H264EncodeOneFrameAsAnnexB) { + SKIP_IF_NOT_SUPPORTED(VIDEO_MP4); + + RefPtr<MediaDataEncoder> e = CreateH264Encoder(); + EnsureInit(e); + + MediaDataEncoder::EncodedData output = Encode(e, 1UL, mData); + EXPECT_EQ(output.Length(), 1UL); + EXPECT_TRUE(AnnexB::IsAnnexB(output[0])); + + WaitForShutdown(e); +} + +TEST_F(MediaDataEncoderTest, EncodeMultipleFramesAsAnnexB) { + SKIP_IF_NOT_SUPPORTED(VIDEO_MP4); + + RefPtr<MediaDataEncoder> e = CreateH264Encoder(); + EnsureInit(e); + + MediaDataEncoder::EncodedData output = Encode(e, NUM_FRAMES, mData); + EXPECT_EQ(output.Length(), NUM_FRAMES); + for (auto frame : output) { + EXPECT_TRUE(AnnexB::IsAnnexB(frame)); + } + + WaitForShutdown(e); +} + +TEST_F(MediaDataEncoderTest, EncodeMultipleFramesAsAVCC) { + SKIP_IF_NOT_SUPPORTED(VIDEO_MP4); + + RefPtr<MediaDataEncoder> e = + CreateH264Encoder(MediaDataEncoder::Usage::Record); + EnsureInit(e); + + MediaDataEncoder::EncodedData output = Encode(e, NUM_FRAMES, mData); + EXPECT_EQ(output.Length(), NUM_FRAMES); + AnnexB::IsAVCC(output[0]); // Only 1st frame has extra data. + for (auto frame : output) { + EXPECT_FALSE(AnnexB::IsAnnexB(frame)); + } + + WaitForShutdown(e); +} + +#ifndef DEBUG // Zero width or height will assert/crash in debug builds. +TEST_F(MediaDataEncoderTest, InvalidSize) { + SKIP_IF_NOT_SUPPORTED(VIDEO_MP4); + + RefPtr<MediaDataEncoder> e0x0 = + CreateH264Encoder(MediaDataEncoder::Usage::Realtime, + MediaDataEncoder::PixelFormat::YUV420P, 0, 0); + EXPECT_NE(e0x0, nullptr); + EXPECT_FALSE(EnsureInit(e0x0)); + + RefPtr<MediaDataEncoder> e0x1 = + CreateH264Encoder(MediaDataEncoder::Usage::Realtime, + MediaDataEncoder::PixelFormat::YUV420P, 0, 1); + EXPECT_NE(e0x1, nullptr); + EXPECT_FALSE(EnsureInit(e0x1)); + + RefPtr<MediaDataEncoder> e1x0 = + CreateH264Encoder(MediaDataEncoder::Usage::Realtime, + MediaDataEncoder::PixelFormat::YUV420P, 1, 0); + EXPECT_NE(e1x0, nullptr); + EXPECT_FALSE(EnsureInit(e1x0)); +} +#endif + +#ifdef MOZ_WIDGET_ANDROID +TEST_F(MediaDataEncoderTest, AndroidNotSupportedSize) { + SKIP_IF_NOT_SUPPORTED(VIDEO_MP4); + + RefPtr<MediaDataEncoder> e = + CreateH264Encoder(MediaDataEncoder::Usage::Realtime, + MediaDataEncoder::PixelFormat::YUV420P, 1, 1); + EXPECT_NE(e, nullptr); + EXPECT_FALSE(EnsureInit(e)); +} +#endif diff --git a/dom/media/gtest/TestMediaEventSource.cpp b/dom/media/gtest/TestMediaEventSource.cpp new file mode 100644 index 0000000000..7b66cb4875 --- /dev/null +++ b/dom/media/gtest/TestMediaEventSource.cpp @@ -0,0 +1,398 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "mozilla/SharedThreadPool.h" +#include "mozilla/TaskQueue.h" +#include "mozilla/UniquePtr.h" +#include "MediaEventSource.h" +#include "VideoUtils.h" + +using namespace mozilla; + +/* + * Test if listeners receive the event data correctly. + */ +TEST(MediaEventSource, SingleListener) +{ + RefPtr<TaskQueue> queue = + new TaskQueue(GetMediaThreadPool(MediaThreadType::SUPERVISOR)); + + MediaEventProducer<int> source; + int i = 0; + + auto func = [&](int j) { i += j; }; + MediaEventListener listener = source.Connect(queue, func); + + // Call Notify 3 times. The listener should be also called 3 times. + source.Notify(3); + source.Notify(5); + source.Notify(7); + + queue->BeginShutdown(); + queue->AwaitShutdownAndIdle(); + + // Verify the event data is passed correctly to the listener. + EXPECT_EQ(i, 15); // 3 + 5 + 7 + listener.Disconnect(); +} + +TEST(MediaEventSource, MultiListener) +{ + RefPtr<TaskQueue> queue = + new TaskQueue(GetMediaThreadPool(MediaThreadType::SUPERVISOR)); + + MediaEventProducer<int> source; + int i = 0; + int j = 0; + + auto func1 = [&](int k) { i = k * 2; }; + auto func2 = [&](int k) { j = k * 3; }; + MediaEventListener listener1 = source.Connect(queue, func1); + MediaEventListener listener2 = source.Connect(queue, func2); + + // Both listeners should receive the event. + source.Notify(11); + + queue->BeginShutdown(); + queue->AwaitShutdownAndIdle(); + + // Verify the event data is passed correctly to the listener. + EXPECT_EQ(i, 22); // 11 * 2 + EXPECT_EQ(j, 33); // 11 * 3 + + listener1.Disconnect(); + listener2.Disconnect(); +} + +/* + * Test if disconnecting a listener prevents events from coming. + */ +TEST(MediaEventSource, DisconnectAfterNotification) +{ + RefPtr<TaskQueue> queue = + new TaskQueue(GetMediaThreadPool(MediaThreadType::SUPERVISOR)); + + MediaEventProducer<int> source; + int i = 0; + + MediaEventListener listener; + auto func = [&](int j) { + i += j; + listener.Disconnect(); + }; + listener = source.Connect(queue, func); + + // Call Notify() twice. Since we disconnect the listener when receiving + // the 1st event, the 2nd event should not reach the listener. + source.Notify(11); + source.Notify(11); + + queue->BeginShutdown(); + queue->AwaitShutdownAndIdle(); + + // Check only the 1st event is received. + EXPECT_EQ(i, 11); +} + +TEST(MediaEventSource, DisconnectBeforeNotification) +{ + RefPtr<TaskQueue> queue = + new TaskQueue(GetMediaThreadPool(MediaThreadType::SUPERVISOR)); + + MediaEventProducer<int> source; + int i = 0; + int j = 0; + + auto func1 = [&](int k) { i = k * 2; }; + auto func2 = [&](int k) { j = k * 3; }; + MediaEventListener listener1 = source.Connect(queue, func1); + MediaEventListener listener2 = source.Connect(queue, func2); + + // Disconnect listener2 before notification. Only listener1 should receive + // the event. + listener2.Disconnect(); + source.Notify(11); + + queue->BeginShutdown(); + queue->AwaitShutdownAndIdle(); + + EXPECT_EQ(i, 22); // 11 * 2 + EXPECT_EQ(j, 0); // event not received + + listener1.Disconnect(); +} + +/* + * Test we don't hit the assertion when calling Connect() and Disconnect() + * repeatedly. + */ +TEST(MediaEventSource, DisconnectAndConnect) +{ + RefPtr<TaskQueue> queue; + MediaEventProducerExc<int> source; + MediaEventListener listener = source.Connect(queue, []() {}); + listener.Disconnect(); + listener = source.Connect(queue, []() {}); + listener.Disconnect(); +} + +/* + * Test void event type. + */ +TEST(MediaEventSource, VoidEventType) +{ + RefPtr<TaskQueue> queue = + new TaskQueue(GetMediaThreadPool(MediaThreadType::SUPERVISOR)); + + MediaEventProducer<void> source; + int i = 0; + + // Test function object. + auto func = [&]() { ++i; }; + MediaEventListener listener1 = source.Connect(queue, func); + + // Test member function. + struct Foo { + Foo() : j(1) {} + void OnNotify() { j *= 2; } + int j; + } foo; + MediaEventListener listener2 = source.Connect(queue, &foo, &Foo::OnNotify); + + // Call Notify 2 times. The listener should be also called 2 times. + source.Notify(); + source.Notify(); + + queue->BeginShutdown(); + queue->AwaitShutdownAndIdle(); + + // Verify the event data is passed correctly to the listener. + EXPECT_EQ(i, 2); // ++i called twice + EXPECT_EQ(foo.j, 4); // |j *= 2| called twice + listener1.Disconnect(); + listener2.Disconnect(); +} + +/* + * Test listeners can take various event types (T, T&&, const T& and void). + */ +TEST(MediaEventSource, ListenerType1) +{ + RefPtr<TaskQueue> queue = + new TaskQueue(GetMediaThreadPool(MediaThreadType::SUPERVISOR)); + + MediaEventProducer<int> source; + int i = 0; + + // Test various argument types. + auto func1 = [&](int&& j) { i += j; }; + auto func2 = [&](const int& j) { i += j; }; + auto func3 = [&]() { i += 1; }; + MediaEventListener listener1 = source.Connect(queue, func1); + MediaEventListener listener2 = source.Connect(queue, func2); + MediaEventListener listener3 = source.Connect(queue, func3); + + source.Notify(1); + + queue->BeginShutdown(); + queue->AwaitShutdownAndIdle(); + + EXPECT_EQ(i, 3); + + listener1.Disconnect(); + listener2.Disconnect(); + listener3.Disconnect(); +} + +TEST(MediaEventSource, ListenerType2) +{ + RefPtr<TaskQueue> queue = + new TaskQueue(GetMediaThreadPool(MediaThreadType::SUPERVISOR)); + + MediaEventProducer<int> source; + + struct Foo { + Foo() : mInt(0) {} + void OnNotify1(int&& i) { mInt += i; } + void OnNotify2(const int& i) { mInt += i; } + void OnNotify3() { mInt += 1; } + void OnNotify4(int i) const { mInt += i; } + void OnNotify5(int i) volatile { mInt += i; } + mutable int mInt; + } foo; + + // Test member functions which might be CV qualified. + MediaEventListener listener1 = source.Connect(queue, &foo, &Foo::OnNotify1); + MediaEventListener listener2 = source.Connect(queue, &foo, &Foo::OnNotify2); + MediaEventListener listener3 = source.Connect(queue, &foo, &Foo::OnNotify3); + MediaEventListener listener4 = source.Connect(queue, &foo, &Foo::OnNotify4); + MediaEventListener listener5 = source.Connect(queue, &foo, &Foo::OnNotify5); + + source.Notify(1); + + queue->BeginShutdown(); + queue->AwaitShutdownAndIdle(); + + EXPECT_EQ(foo.mInt, 5); + + listener1.Disconnect(); + listener2.Disconnect(); + listener3.Disconnect(); + listener4.Disconnect(); + listener5.Disconnect(); +} + +struct SomeEvent { + explicit SomeEvent(int& aCount) : mCount(aCount) {} + // Increment mCount when copy constructor is called to know how many times + // the event data is copied. + SomeEvent(const SomeEvent& aOther) : mCount(aOther.mCount) { ++mCount; } + SomeEvent(SomeEvent&& aOther) : mCount(aOther.mCount) {} + int& mCount; +}; + +/* + * Test we don't have unnecessary copies of the event data. + */ +TEST(MediaEventSource, CopyEvent1) +{ + RefPtr<TaskQueue> queue = + new TaskQueue(GetMediaThreadPool(MediaThreadType::SUPERVISOR)); + + MediaEventProducer<SomeEvent> source; + int i = 0; + + auto func = [](SomeEvent&& aEvent) {}; + struct Foo { + void OnNotify(SomeEvent&& aEvent) {} + } foo; + + MediaEventListener listener1 = source.Connect(queue, func); + MediaEventListener listener2 = source.Connect(queue, &foo, &Foo::OnNotify); + + // We expect i to be 2 since SomeEvent should be copied only once when + // passing to each listener. + source.Notify(SomeEvent(i)); + + queue->BeginShutdown(); + queue->AwaitShutdownAndIdle(); + EXPECT_EQ(i, 2); + listener1.Disconnect(); + listener2.Disconnect(); +} + +TEST(MediaEventSource, CopyEvent2) +{ + RefPtr<TaskQueue> queue = + new TaskQueue(GetMediaThreadPool(MediaThreadType::SUPERVISOR)); + + MediaEventProducer<SomeEvent> source; + int i = 0; + + auto func = []() {}; + struct Foo { + void OnNotify() {} + } foo; + + MediaEventListener listener1 = source.Connect(queue, func); + MediaEventListener listener2 = source.Connect(queue, &foo, &Foo::OnNotify); + + // SomeEvent won't be copied at all since the listeners take no arguments. + source.Notify(SomeEvent(i)); + + queue->BeginShutdown(); + queue->AwaitShutdownAndIdle(); + EXPECT_EQ(i, 0); + listener1.Disconnect(); + listener2.Disconnect(); +} + +/* + * Test move-only types. + */ +TEST(MediaEventSource, MoveOnly) +{ + RefPtr<TaskQueue> queue = + new TaskQueue(GetMediaThreadPool(MediaThreadType::SUPERVISOR)); + + MediaEventProducerExc<UniquePtr<int>> source; + + auto func = [](UniquePtr<int>&& aEvent) { EXPECT_EQ(*aEvent, 20); }; + MediaEventListener listener = source.Connect(queue, func); + + // It is OK to pass an rvalue which is move-only. + source.Notify(UniquePtr<int>(new int(20))); + // It is an error to pass an lvalue which is move-only. + // UniquePtr<int> event(new int(30)); + // source.Notify(event); + + queue->BeginShutdown(); + queue->AwaitShutdownAndIdle(); + listener.Disconnect(); +} + +struct RefCounter { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RefCounter) + explicit RefCounter(int aVal) : mVal(aVal) {} + int mVal; + + private: + ~RefCounter() = default; +}; + +/* + * Test we should copy instead of move in NonExclusive mode + * for each listener must get a copy. + */ +TEST(MediaEventSource, NoMove) +{ + RefPtr<TaskQueue> queue = + new TaskQueue(GetMediaThreadPool(MediaThreadType::SUPERVISOR)); + + MediaEventProducer<RefPtr<RefCounter>> source; + + auto func1 = [](RefPtr<RefCounter>&& aEvent) { EXPECT_EQ(aEvent->mVal, 20); }; + auto func2 = [](RefPtr<RefCounter>&& aEvent) { EXPECT_EQ(aEvent->mVal, 20); }; + MediaEventListener listener1 = source.Connect(queue, func1); + MediaEventListener listener2 = source.Connect(queue, func2); + + // We should copy this rvalue instead of move it in NonExclusive mode. + RefPtr<RefCounter> val = new RefCounter(20); + source.Notify(std::move(val)); + + queue->BeginShutdown(); + queue->AwaitShutdownAndIdle(); + listener1.Disconnect(); + listener2.Disconnect(); +} + +/* + * Rvalue lambda should be moved instead of copied. + */ +TEST(MediaEventSource, MoveLambda) +{ + RefPtr<TaskQueue> queue; + MediaEventProducer<void> source; + + int counter = 0; + SomeEvent someEvent(counter); + + auto func = [someEvent]() {}; + // someEvent is copied when captured by the lambda. + EXPECT_EQ(someEvent.mCount, 1); + + // someEvent should be copied for we pass |func| as an lvalue. + MediaEventListener listener1 = source.Connect(queue, func); + EXPECT_EQ(someEvent.mCount, 2); + + // someEvent should be moved for we pass |func| as an rvalue. + MediaEventListener listener2 = source.Connect(queue, std::move(func)); + EXPECT_EQ(someEvent.mCount, 2); + + listener1.Disconnect(); + listener2.Disconnect(); +} diff --git a/dom/media/gtest/TestMediaMIMETypes.cpp b/dom/media/gtest/TestMediaMIMETypes.cpp new file mode 100644 index 0000000000..d36e3bf586 --- /dev/null +++ b/dom/media/gtest/TestMediaMIMETypes.cpp @@ -0,0 +1,284 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "MediaMIMETypes.h" +#include "mozilla/Unused.h" + +using namespace mozilla; + +TEST(MediaMIMETypes, DependentMIMEType) +{ + static const struct { + const char* mString; + DependentMediaMIMEType mDependentMediaMIMEType; + } tests[] = {{"audio/mp4", MEDIAMIMETYPE("audio/mp4")}, + {"video/mp4", MEDIAMIMETYPE("video/mp4")}, + {"application/x-mp4", MEDIAMIMETYPE("application/x-mp4")}}; + for (const auto& test : tests) { + EXPECT_TRUE(test.mDependentMediaMIMEType.AsDependentString().EqualsASCII( + test.mString)); + MediaMIMEType mimetype(test.mDependentMediaMIMEType); + EXPECT_TRUE(mimetype.AsString().Equals( + test.mDependentMediaMIMEType.AsDependentString())); + EXPECT_EQ(mimetype, test.mDependentMediaMIMEType); + EXPECT_EQ(mimetype, MediaMIMEType(test.mDependentMediaMIMEType)); + } +} + +TEST(MediaMIMETypes, MakeMediaMIMEType_bad) +{ + static const char* tests[] = {"", " ", "/", "audio", + "audio/", "mp4", "/mp4", "a/b"}; + + for (const auto& test : tests) { + Maybe<MediaMIMEType> type = MakeMediaMIMEType(test); + EXPECT_TRUE(type.isNothing()) + << "MakeMediaMIMEType(\"" << test << "\").isNothing()"; + } +} + +TEST(MediaMIMETypes, MediaMIMEType) +{ + static const struct { + const char* mTypeString; + const char* mAsString; + bool mApplication; + bool mAudio; + bool mVideo; + bool mEqualsLiteralVideoSlashMp4; // tests `== "video/mp4"` + } tests[] = { + // in AsString app audio video ==v/mp4 + {"video/mp4", "video/mp4", false, false, true, true}, + {"video/mp4; codecs=0", "video/mp4", false, false, true, true}, + {"VIDEO/MP4", "video/mp4", false, false, true, true}, + {"audio/mp4", "audio/mp4", false, true, false, false}, + {"application/x", "application/x", true, false, false, false}}; + + for (const auto& test : tests) { + Maybe<MediaMIMEType> type = MakeMediaMIMEType(test.mTypeString); + EXPECT_TRUE(type.isSome()) + << "MakeMediaMIMEType(\"" << test.mTypeString << "\").isSome()"; + EXPECT_TRUE(type->AsString().EqualsASCII(test.mAsString)) + << "MakeMediaMIMEType(\"" << test.mTypeString << "\")->AsString() == \"" + << test.mAsString << "\""; + EXPECT_EQ(test.mApplication, type->HasApplicationMajorType()) + << "MakeMediaMIMEType(\"" << test.mTypeString + << "\")->HasApplicationMajorType() == " + << (test.mApplication ? "true" : "false"); + EXPECT_EQ(test.mAudio, type->HasAudioMajorType()) + << "MakeMediaMIMEType(\"" << test.mTypeString + << "\")->HasAudioMajorType() == " << (test.mAudio ? "true" : "false"); + EXPECT_EQ(test.mVideo, type->HasVideoMajorType()) + << "MakeMediaMIMEType(\"" << test.mTypeString + << "\")->HasVideoMajorType() == " << (test.mVideo ? "true" : "false"); + EXPECT_EQ(test.mEqualsLiteralVideoSlashMp4, + *type == MEDIAMIMETYPE("video/mp4")) + << "*MakeMediaMIMEType(\"" << test.mTypeString + << "\") == MEDIAMIMETYPE(\"video/mp4\")"; + } +} + +TEST(MediaMIMETypes, MediaCodecs) +{ + MediaCodecs empty(""); + EXPECT_TRUE(empty.IsEmpty()); + EXPECT_TRUE(empty.AsString().EqualsLiteral("")); + EXPECT_FALSE(empty.Contains(u""_ns)); + EXPECT_FALSE(empty.Contains(u"c1"_ns)); + EXPECT_FALSE(empty.ContainsPrefix(u""_ns)); + EXPECT_FALSE(empty.ContainsPrefix(u"c1"_ns)); + int iterations = 0; + for (const auto& codec : empty.Range()) { + ++iterations; + Unused << codec; + } + EXPECT_EQ(0, iterations); + + MediaCodecs space(" "); + EXPECT_FALSE(space.IsEmpty()); + EXPECT_TRUE(space.AsString().EqualsLiteral(" ")); + EXPECT_TRUE(space.Contains(u""_ns)); + EXPECT_FALSE(space.Contains(u"c1"_ns)); + EXPECT_TRUE(space.ContainsPrefix(u""_ns)); + EXPECT_FALSE(space.ContainsPrefix(u"c"_ns)); + EXPECT_FALSE(space.ContainsPrefix(u"c1"_ns)); + iterations = 0; + for (const auto& codec : space.Range()) { + ++iterations; + EXPECT_TRUE(codec.IsEmpty()); + } + EXPECT_EQ(1, iterations); + + MediaCodecs one(" c1 "); + EXPECT_FALSE(one.IsEmpty()); + EXPECT_TRUE(one.AsString().EqualsLiteral(" c1 ")); + EXPECT_FALSE(one.Contains(u""_ns)); + EXPECT_TRUE(one.Contains(u"c1"_ns)); + EXPECT_TRUE(one.ContainsPrefix(u""_ns)); + EXPECT_TRUE(one.ContainsPrefix(u"c"_ns)); + EXPECT_TRUE(one.ContainsPrefix(u"c1"_ns)); + EXPECT_FALSE(one.ContainsPrefix(u"c1x"_ns)); + EXPECT_FALSE(one.ContainsPrefix(u"c1 "_ns)); + iterations = 0; + for (const auto& codec : one.Range()) { + ++iterations; + EXPECT_TRUE(codec.EqualsLiteral("c1")); + } + EXPECT_EQ(1, iterations); + + MediaCodecs two(" c1 , c2 "); + EXPECT_FALSE(two.IsEmpty()); + EXPECT_TRUE(two.AsString().EqualsLiteral(" c1 , c2 ")); + EXPECT_FALSE(two.Contains(u""_ns)); + EXPECT_TRUE(two.Contains(u"c1"_ns)); + EXPECT_TRUE(two.Contains(u"c2"_ns)); + EXPECT_TRUE(two.ContainsPrefix(u""_ns)); + EXPECT_TRUE(two.ContainsPrefix(u"c"_ns)); + EXPECT_FALSE(two.ContainsPrefix(u"1"_ns)); + EXPECT_TRUE(two.ContainsPrefix(u"c1"_ns)); + EXPECT_TRUE(two.ContainsPrefix(u"c2"_ns)); + EXPECT_FALSE(two.ContainsPrefix(u"c1x"_ns)); + EXPECT_FALSE(two.ContainsPrefix(u"c2x"_ns)); + iterations = 0; + for (const auto& codec : two.Range()) { + ++iterations; + char buffer[] = "c0"; + buffer[1] += iterations; + EXPECT_TRUE(codec.EqualsASCII(buffer)); + } + EXPECT_EQ(2, iterations); + + EXPECT_TRUE(two.ContainsAll(two)); + EXPECT_TRUE(two.ContainsAll(one)); + EXPECT_FALSE(one.ContainsAll(two)); + + // Check wide char case where both octets/bytes are relevant. Note we don't + // use `EqualsLiteral` here because at the time of writing it will place the + // literal into a narrow string which then doesn't compare correctly with + // the wide representation from MediaCodecs. + MediaCodecs euroSign(" € "); // U+20AC + EXPECT_FALSE(euroSign.IsEmpty()); + EXPECT_TRUE(euroSign.AsString().Equals(u" € "_ns)); + EXPECT_FALSE(euroSign.Contains(u""_ns)); + EXPECT_TRUE(euroSign.Contains(u"€"_ns)); + EXPECT_FALSE(euroSign.Contains(u"€€"_ns)); + EXPECT_TRUE(euroSign.ContainsPrefix(u""_ns)); + EXPECT_TRUE(euroSign.ContainsPrefix(u"€"_ns)); + EXPECT_FALSE(euroSign.ContainsPrefix( + u"â‚"_ns)); // U+20AD -- ensure second octet is compared + EXPECT_FALSE(euroSign.ContainsPrefix( + u"↬"_ns)); // U+21AC -- ensure first octet is compared + EXPECT_FALSE(euroSign.ContainsPrefix(u"€ "_ns)); + iterations = 0; + for (const auto& codec : euroSign.Range()) { + ++iterations; + EXPECT_TRUE(codec.Equals(u"€"_ns)); + } + EXPECT_EQ(1, iterations); +} + +TEST(MediaMIMETypes, MakeMediaExtendedMIMEType_bad) +{ + static const char* tests[] = {"", " ", "/", "audio", + "audio/", "mp4", "/mp4", "a/b"}; + + for (const auto& test : tests) { + Maybe<MediaExtendedMIMEType> type = MakeMediaExtendedMIMEType(test); + EXPECT_TRUE(type.isNothing()) + << "MakeMediaExtendedMIMEType(\"" << test << "\").isNothing()"; + } +} + +TEST(MediaMIMETypes, MediaExtendedMIMEType) +{ + // Some generic tests first. + static const struct { + const char* mTypeString; + const char* mTypeAsString; + bool mApplication; + bool mAudio; + bool mVideo; + bool mEqualsLiteralVideoSlashMp4; // tests `== "video/mp4"` + bool mHaveCodecs; + } tests[] = { + // in Type().AsString app audio video ==v/mp4 + // codecs + {"video/mp4", "video/mp4", false, false, true, true, false}, + {"video/mp4; codecs=0", "video/mp4", false, false, true, true, true}, + {"VIDEO/MP4", "video/mp4", false, false, true, true, false}, + {"audio/mp4", "audio/mp4", false, true, false, false, false}, + {"video/webm", "video/webm", false, false, true, false, false}, + {"audio/webm", "audio/webm", false, true, false, false, false}, + {"application/x", "application/x", true, false, false, false, false}}; + + for (const auto& test : tests) { + Maybe<MediaExtendedMIMEType> type = + MakeMediaExtendedMIMEType(test.mTypeString); + EXPECT_TRUE(type.isSome()) + << "MakeMediaExtendedMIMEType(\"" << test.mTypeString << "\").isSome()"; + EXPECT_TRUE(type->OriginalString().EqualsASCII(test.mTypeString)) + << "MakeMediaExtendedMIMEType(\"" << test.mTypeString + << "\")->AsString() == \"" << test.mTypeAsString << "\""; + EXPECT_TRUE(type->Type().AsString().EqualsASCII(test.mTypeAsString)) + << "MakeMediaExtendedMIMEType(\"" << test.mTypeString + << "\")->AsString() == \"" << test.mTypeAsString << "\""; + EXPECT_EQ(test.mApplication, type->Type().HasApplicationMajorType()) + << "MakeMediaExtendedMIMEType(\"" << test.mTypeString + << "\")->Type().HasApplicationMajorType() == " + << (test.mApplication ? "true" : "false"); + EXPECT_EQ(test.mAudio, type->Type().HasAudioMajorType()) + << "MakeMediaExtendedMIMEType(\"" << test.mTypeString + << "\")->Type().HasAudioMajorType() == " + << (test.mAudio ? "true" : "false"); + EXPECT_EQ(test.mVideo, type->Type().HasVideoMajorType()) + << "MakeMediaExtendedMIMEType(\"" << test.mTypeString + << "\")->Type().HasVideoMajorType() == " + << (test.mVideo ? "true" : "false"); + EXPECT_EQ(test.mEqualsLiteralVideoSlashMp4, + type->Type() == MEDIAMIMETYPE("video/mp4")) + << "*MakeMediaExtendedMIMEType(\"" << test.mTypeString + << "\")->Type() == MEDIAMIMETYPE(\"video/mp4\")"; + EXPECT_EQ(test.mHaveCodecs, type->HaveCodecs()) + << "MakeMediaExtendedMIMEType(\"" << test.mTypeString + << "\")->HaveCodecs() == " << (test.mHaveCodecs ? "true" : "false"); + EXPECT_NE(test.mHaveCodecs, type->Codecs().IsEmpty()) + << "MakeMediaExtendedMIMEType(\"" << test.mTypeString + << "\")->Codecs.IsEmpty() != " << (test.mHaveCodecs ? "true" : "false"); + EXPECT_FALSE(type->GetWidth()) << "MakeMediaExtendedMIMEType(\"" + << test.mTypeString << "\")->GetWidth()"; + EXPECT_FALSE(type->GetHeight()) << "MakeMediaExtendedMIMEType(\"" + << test.mTypeString << "\")->GetHeight()"; + EXPECT_FALSE(type->GetFramerate()) + << "MakeMediaExtendedMIMEType(\"" << test.mTypeString + << "\")->GetFramerate()"; + EXPECT_FALSE(type->GetBitrate()) << "MakeMediaExtendedMIMEType(\"" + << test.mTypeString << "\")->GetBitrate()"; + } + + // Test all extra parameters. + Maybe<MediaExtendedMIMEType> type = MakeMediaExtendedMIMEType( + "video/mp4; codecs=\"a,b\"; width=1024; Height=768; FrameRate=60; " + "BITRATE=100000"); + EXPECT_TRUE(type->HaveCodecs()); + EXPECT_FALSE(type->Codecs().IsEmpty()); + EXPECT_TRUE(type->Codecs().AsString().EqualsASCII("a,b")); + EXPECT_TRUE(type->Codecs() == "a,b"); + EXPECT_TRUE(type->Codecs().Contains(u"a"_ns)); + EXPECT_TRUE(type->Codecs().Contains(u"b"_ns)); + EXPECT_TRUE(type->Codecs().ContainsPrefix(u"a"_ns)); + EXPECT_TRUE(type->Codecs().ContainsPrefix(u"b"_ns)); + EXPECT_FALSE(type->Codecs().ContainsPrefix(u"ab"_ns)); + EXPECT_FALSE(type->Codecs().ContainsPrefix(u"ba"_ns)); + EXPECT_FALSE(type->Codecs().ContainsPrefix(u"a,b"_ns)); + EXPECT_TRUE(!!type->GetWidth()); + EXPECT_EQ(1024, *type->GetWidth()); + EXPECT_TRUE(!!type->GetHeight()); + EXPECT_EQ(768, *type->GetHeight()); + EXPECT_TRUE(!!type->GetFramerate()); + EXPECT_EQ(60, *type->GetFramerate()); + EXPECT_TRUE(!!type->GetBitrate()); + EXPECT_EQ(100000, *type->GetBitrate()); +} diff --git a/dom/media/gtest/TestMediaSpan.cpp b/dom/media/gtest/TestMediaSpan.cpp new file mode 100644 index 0000000000..e6edcb944b --- /dev/null +++ b/dom/media/gtest/TestMediaSpan.cpp @@ -0,0 +1,110 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <gtest/gtest.h> +#include <stdint.h> + +#include "MediaSpan.h" + +#include "mozilla/ArrayUtils.h" + +using namespace mozilla; + +already_AddRefed<MediaByteBuffer> makeBuffer(uint8_t aStart, uint8_t aEnd) { + RefPtr<MediaByteBuffer> buffer(new MediaByteBuffer); + for (uint8_t i = aStart; i <= aEnd; i++) { + buffer->AppendElement(i); + } + return buffer.forget(); +} + +bool IsRangeAt(const MediaSpan& aSpan, uint8_t aStart, uint8_t aEnd, + size_t aAt) { + size_t length = size_t(aEnd) - size_t(aStart) + 1; + if (aAt + length > aSpan.Length()) { + return false; + } + for (size_t i = 0; i < length; i++) { + if (aSpan[aAt + i] != uint8_t(aStart + i)) { + return false; + } + } + return true; +} + +bool IsRange(const MediaSpan& aSpan, uint8_t aStart, uint8_t aEnd) { + return IsRangeAt(aSpan, aStart, aEnd, 0); +} + +TEST(MediaSpan, AppendToFromSpan) +{ + RefPtr<MediaByteBuffer> buffer1 = makeBuffer(0, 9); + MediaSpan span1 = MediaSpan(buffer1); + EXPECT_EQ(span1.Length(), size_t(10)); + EXPECT_TRUE(IsRange(span1, 0, 9)); + + MediaSpan span2 = span1.From(5); + + EXPECT_EQ(span2.Length(), size_t(5)); + EXPECT_TRUE(IsRange(span2, 5, 9)); + RefPtr<MediaByteBuffer> buffer2 = makeBuffer(10, 19); + EXPECT_EQ(buffer2->Length(), size_t(10)); + span2.Append(buffer2); + + // Span2 should be: [5...19] + EXPECT_EQ(span2.Length(), size_t(15)); + EXPECT_TRUE(IsRange(span2, 5, 19)); + + // Span1 should not be modified by the append to span2. + EXPECT_EQ(span1.Length(), size_t(10)); + EXPECT_TRUE(IsRange(span1, 0, 9)); +} + +TEST(MediaSpan, AppendToToSpan) +{ + RefPtr<MediaByteBuffer> buffer1 = makeBuffer(0, 9); + MediaSpan span1 = MediaSpan(buffer1); + EXPECT_EQ(span1.Length(), size_t(10)); + EXPECT_TRUE(IsRange(span1, 0, 9)); + + MediaSpan span2 = span1.To(5); + + // Span2 should be [0...4] + EXPECT_EQ(span2.Length(), size_t(5)); + EXPECT_TRUE(IsRange(span2, 0, 4)); + RefPtr<MediaByteBuffer> buffer2 = makeBuffer(10, 19); + EXPECT_EQ(buffer2->Length(), size_t(10)); + span2.Append(buffer2); + + // Span2 should be: [0...4][10...19] + EXPECT_EQ(span2.Length(), size_t(15)); + EXPECT_TRUE(IsRangeAt(span2, 0, 4, 0)); + EXPECT_TRUE(IsRangeAt(span2, 10, 19, 5)); + + // Span1 should not be modified by the append to span2. + EXPECT_EQ(span1.Length(), size_t(10)); + EXPECT_TRUE(IsRange(span1, 0, 9)); +} + +TEST(MediaSpan, RemoveFront) +{ + RefPtr<MediaByteBuffer> buffer1 = makeBuffer(0, 9); + MediaSpan span1 = MediaSpan(buffer1); + EXPECT_EQ(span1.Length(), size_t(10)); + EXPECT_TRUE(IsRange(span1, 0, 9)); + + MediaSpan span2(span1); + EXPECT_EQ(span2.Length(), size_t(10)); + + span2.RemoveFront(5); + + // Span2 should now be [5...9] + EXPECT_EQ(span2.Length(), size_t(5)); + EXPECT_TRUE(IsRange(span2, 5, 9)); + + // Span1 should be unaffected. + EXPECT_EQ(span1.Length(), size_t(10)); + EXPECT_TRUE(IsRange(span1, 0, 9)); +} diff --git a/dom/media/gtest/TestMuxer.cpp b/dom/media/gtest/TestMuxer.cpp new file mode 100644 index 0000000000..ed456d2c55 --- /dev/null +++ b/dom/media/gtest/TestMuxer.cpp @@ -0,0 +1,224 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <vector> + +#include "ContainerWriter.h" +#include "EncodedFrame.h" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "Muxer.h" +#include "OpusTrackEncoder.h" +#include "WebMWriter.h" + +using namespace mozilla; +using media::TimeUnit; +using testing::_; +using testing::ElementsAre; +using testing::Return; +using testing::StaticAssertTypeEq; + +static RefPtr<TrackMetadataBase> CreateOpusMetadata(int32_t aChannels, + float aSamplingFrequency, + size_t aIdHeaderSize, + size_t aCommentHeaderSize) { + auto opusMetadata = MakeRefPtr<OpusMetadata>(); + opusMetadata->mChannels = aChannels; + opusMetadata->mSamplingFrequency = aSamplingFrequency; + opusMetadata->mIdHeader.SetLength(aIdHeaderSize); + for (size_t i = 0; i < opusMetadata->mIdHeader.Length(); i++) { + opusMetadata->mIdHeader[i] = 0; + } + opusMetadata->mCommentHeader.SetLength(aCommentHeaderSize); + for (size_t i = 0; i < opusMetadata->mCommentHeader.Length(); i++) { + opusMetadata->mCommentHeader[i] = 0; + } + return opusMetadata; +} + +static RefPtr<TrackMetadataBase> CreateVP8Metadata(int32_t aWidth, + int32_t aHeight) { + auto vp8Metadata = MakeRefPtr<VP8Metadata>(); + vp8Metadata->mWidth = aWidth; + vp8Metadata->mDisplayWidth = aWidth; + vp8Metadata->mHeight = aHeight; + vp8Metadata->mDisplayHeight = aHeight; + return vp8Metadata; +} + +static RefPtr<EncodedFrame> CreateFrame(EncodedFrame::FrameType aType, + const TimeUnit& aTime, + const TimeUnit& aDuration, + size_t aDataSize) { + auto data = MakeRefPtr<EncodedFrame::FrameData>(); + data->SetLength(aDataSize); + if (aType == EncodedFrame::OPUS_AUDIO_FRAME) { + // Opus duration is in samples, so figure out how many samples will put us + // closest to aDurationUs without going over. + return MakeRefPtr<EncodedFrame>(aTime, + TimeUnitToFrames(aDuration, 48000).value(), + 48000, aType, std::move(data)); + } + return MakeRefPtr<EncodedFrame>( + aTime, TimeUnitToFrames(aDuration, USECS_PER_S).value(), USECS_PER_S, + aType, std::move(data)); +} + +namespace testing::internal { +// This makes the googletest framework treat nsTArray as an std::vector, so all +// the regular Matchers (like ElementsAre) work for it. +template <typename Element> +class StlContainerView<nsTArray<Element>> { + public: + typedef GTEST_REMOVE_CONST_(Element) RawElement; + typedef std::vector<RawElement> type; + typedef const type const_reference; + static const_reference ConstReference(const nsTArray<Element>& aContainer) { + StaticAssertTypeEq<Element, RawElement>(); + return type(aContainer.begin(), aContainer.end()); + } + static type Copy(const nsTArray<Element>& aContainer) { + return type(aContainer.begin(), aContainer.end()); + } +}; +} // namespace testing::internal + +class MockContainerWriter : public ContainerWriter { + public: + MOCK_METHOD2(WriteEncodedTrack, + nsresult(const nsTArray<RefPtr<EncodedFrame>>&, uint32_t)); + MOCK_METHOD1(SetMetadata, + nsresult(const nsTArray<RefPtr<TrackMetadataBase>>&)); + MOCK_METHOD0(IsWritingComplete, bool()); + MOCK_METHOD2(GetContainerData, + nsresult(nsTArray<nsTArray<uint8_t>>*, uint32_t)); +}; + +TEST(MuxerTest, AudioOnly) +{ + MockContainerWriter* writer = new MockContainerWriter(); + Muxer muxer(WrapUnique<ContainerWriter>(writer)); + + // Prepare data + + auto opusMeta = CreateOpusMetadata(1, 48000, 16, 16); + auto audioFrame = + CreateFrame(EncodedFrame::OPUS_AUDIO_FRAME, TimeUnit::FromSeconds(0), + TimeUnit::FromSeconds(0.2), 4096); + + // Expectations + + EXPECT_CALL(*writer, SetMetadata(ElementsAre(opusMeta))) + .WillOnce(Return(NS_OK)); + EXPECT_CALL(*writer, WriteEncodedTrack(ElementsAre(audioFrame), + ContainerWriter::END_OF_STREAM)) + .WillOnce(Return(NS_OK)); + EXPECT_CALL(*writer, GetContainerData(_, ContainerWriter::GET_HEADER)) + .WillOnce(Return(NS_OK)); + EXPECT_CALL(*writer, GetContainerData(_, ContainerWriter::FLUSH_NEEDED)) + .WillOnce(Return(NS_OK)); + EXPECT_CALL(*writer, IsWritingComplete()).Times(0); + + // Test + + EXPECT_EQ(muxer.SetMetadata(nsTArray<RefPtr<TrackMetadataBase>>({opusMeta})), + NS_OK); + muxer.AddEncodedAudioFrame(audioFrame); + muxer.AudioEndOfStream(); + nsTArray<nsTArray<uint8_t>> buffers; + EXPECT_EQ(muxer.GetData(&buffers), NS_OK); +} + +TEST(MuxerTest, AudioVideo) +{ + MockContainerWriter* writer = new MockContainerWriter(); + Muxer muxer(WrapUnique<ContainerWriter>(writer)); + + // Prepare data + + auto opusMeta = CreateOpusMetadata(1, 48000, 16, 16); + auto vp8Meta = CreateVP8Metadata(640, 480); + auto audioFrame = + CreateFrame(EncodedFrame::OPUS_AUDIO_FRAME, TimeUnit::FromSeconds(0), + TimeUnit::FromSeconds(0.2), 4096); + auto videoFrame = + CreateFrame(EncodedFrame::VP8_I_FRAME, TimeUnit::FromSeconds(0), + TimeUnit::FromSeconds(0.05), 65536); + + // Expectations + + EXPECT_CALL(*writer, SetMetadata(ElementsAre(opusMeta, vp8Meta))) + .WillOnce(Return(NS_OK)); + EXPECT_CALL(*writer, WriteEncodedTrack(ElementsAre(videoFrame, audioFrame), + ContainerWriter::END_OF_STREAM)) + .WillOnce(Return(NS_OK)); + EXPECT_CALL(*writer, GetContainerData(_, ContainerWriter::GET_HEADER)) + .WillOnce(Return(NS_OK)); + EXPECT_CALL(*writer, GetContainerData(_, ContainerWriter::FLUSH_NEEDED)) + .WillOnce(Return(NS_OK)); + EXPECT_CALL(*writer, IsWritingComplete()).Times(0); + + // Test + + EXPECT_EQ(muxer.SetMetadata( + nsTArray<RefPtr<TrackMetadataBase>>({opusMeta, vp8Meta})), + NS_OK); + muxer.AddEncodedAudioFrame(audioFrame); + muxer.AudioEndOfStream(); + muxer.AddEncodedVideoFrame(videoFrame); + muxer.VideoEndOfStream(); + nsTArray<nsTArray<uint8_t>> buffers; + EXPECT_EQ(muxer.GetData(&buffers), NS_OK); +} + +TEST(MuxerTest, AudioVideoOutOfOrder) +{ + MockContainerWriter* writer = new MockContainerWriter(); + Muxer muxer(WrapUnique<ContainerWriter>(writer)); + + // Prepare data + + auto opusMeta = CreateOpusMetadata(1, 48000, 16, 16); + auto vp8Meta = CreateVP8Metadata(640, 480); + auto a0 = + CreateFrame(EncodedFrame::OPUS_AUDIO_FRAME, TimeUnit::FromMicroseconds(0), + TimeUnit::FromMicroseconds(48), 4096); + auto v0 = + CreateFrame(EncodedFrame::VP8_I_FRAME, TimeUnit::FromMicroseconds(0), + TimeUnit::FromMicroseconds(50), 65536); + auto a48 = CreateFrame(EncodedFrame::OPUS_AUDIO_FRAME, + TimeUnit::FromMicroseconds(48), + TimeUnit::FromMicroseconds(48), 4096); + auto v50 = + CreateFrame(EncodedFrame::VP8_I_FRAME, TimeUnit::FromMicroseconds(50), + TimeUnit::FromMicroseconds(50), 65536); + + // Expectations + + EXPECT_CALL(*writer, SetMetadata(ElementsAre(opusMeta, vp8Meta))) + .WillOnce(Return(NS_OK)); + EXPECT_CALL(*writer, WriteEncodedTrack(ElementsAre(v0, a0, a48, v50), + ContainerWriter::END_OF_STREAM)) + .WillOnce(Return(NS_OK)); + EXPECT_CALL(*writer, GetContainerData(_, ContainerWriter::GET_HEADER)) + .WillOnce(Return(NS_OK)); + EXPECT_CALL(*writer, GetContainerData(_, ContainerWriter::FLUSH_NEEDED)) + .WillOnce(Return(NS_OK)); + EXPECT_CALL(*writer, IsWritingComplete()).Times(0); + + // Test + + EXPECT_EQ(muxer.SetMetadata( + nsTArray<RefPtr<TrackMetadataBase>>({opusMeta, vp8Meta})), + NS_OK); + muxer.AddEncodedAudioFrame(a0); + muxer.AddEncodedVideoFrame(v0); + muxer.AddEncodedVideoFrame(v50); + muxer.VideoEndOfStream(); + muxer.AddEncodedAudioFrame(a48); + muxer.AudioEndOfStream(); + nsTArray<nsTArray<uint8_t>> buffers; + EXPECT_EQ(muxer.GetData(&buffers), NS_OK); +} diff --git a/dom/media/gtest/TestOpusParser.cpp b/dom/media/gtest/TestOpusParser.cpp new file mode 100644 index 0000000000..639fe7cfc0 --- /dev/null +++ b/dom/media/gtest/TestOpusParser.cpp @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "OpusParser.h" +#include <algorithm> + +using namespace mozilla; + +TEST(OpusParser, Mapping2) +{ + uint8_t validChannels[] = {1, 3, 4, 6, 9, 11, 16, 18, 25, 27, + 36, 38, 49, 51, 64, 66, 81, 83, 100, 102, + 121, 123, 144, 146, 169, 171, 196, 198, 225, 227}; + for (uint8_t channels = 0; channels < 255; channels++) { + bool found = OpusParser::IsValidMapping2ChannelsCount(channels); + bool foundTable = + std::find(std::begin(validChannels), std::end(validChannels), + channels) != std::end(validChannels); + EXPECT_EQ(found, foundTable); + } +} diff --git a/dom/media/gtest/TestRust.cpp b/dom/media/gtest/TestRust.cpp new file mode 100644 index 0000000000..059500767f --- /dev/null +++ b/dom/media/gtest/TestRust.cpp @@ -0,0 +1,10 @@ +#include <stdint.h> +#include "gtest/gtest.h" + +extern "C" uint8_t* test_rust(); + +TEST(rust, CallFromCpp) +{ + auto greeting = test_rust(); + EXPECT_STREQ(reinterpret_cast<char*>(greeting), "hello from rust."); +} diff --git a/dom/media/gtest/TestTimeUnit.cpp b/dom/media/gtest/TestTimeUnit.cpp new file mode 100644 index 0000000000..03243cb0e4 --- /dev/null +++ b/dom/media/gtest/TestTimeUnit.cpp @@ -0,0 +1,71 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "TimeUnits.h" +#include <algorithm> +#include <vector> + +using namespace mozilla; +using namespace mozilla::media; + +TEST(TimeUnit, Rounding) +{ + int64_t usecs = 66261715; + double seconds = media::TimeUnit::FromMicroseconds(usecs).ToSeconds(); + EXPECT_EQ(media::TimeUnit::FromSeconds(seconds).ToMicroseconds(), usecs); + + seconds = 4.169470; + usecs = 4169470; + EXPECT_EQ(media::TimeUnit::FromSeconds(seconds).ToMicroseconds(), usecs); +} + +TEST(TimeUnit, InfinityMath) +{ + // Operator plus/minus uses floating point behaviour for positive and + // negative infinity values, i.e.: + // posInf + posInf = inf + // posInf + negInf = -nan + // posInf + finite = inf + // posInf - posInf = -nan + // posInf - negInf = inf + // posInf - finite = inf + // negInf + negInf = -inf + // negInf + posInf = -nan + // negInf + finite = -inf + // negInf - negInf = -nan + // negInf - posInf = -inf + // negInf - finite = -inf + // finite + posInf = inf + // finite - posInf = -inf + // finite + negInf = -inf + // finite - negInf = inf + + const TimeUnit posInf = TimeUnit::FromInfinity(); + EXPECT_EQ(TimeUnit::FromSeconds(mozilla::PositiveInfinity<double>()), posInf); + + const TimeUnit negInf = TimeUnit::FromNegativeInfinity(); + EXPECT_EQ(TimeUnit::FromSeconds(mozilla::NegativeInfinity<double>()), negInf); + + EXPECT_EQ(posInf + posInf, posInf); + EXPECT_FALSE((posInf + negInf).IsValid()); + EXPECT_FALSE((posInf - posInf).IsValid()); + EXPECT_EQ(posInf - negInf, posInf); + EXPECT_EQ(negInf + negInf, negInf); + EXPECT_FALSE((negInf + posInf).IsValid()); + EXPECT_FALSE((negInf - negInf).IsValid()); + EXPECT_EQ(negInf - posInf, negInf); + + const TimeUnit finite = TimeUnit::FromSeconds(42.0); + EXPECT_EQ(posInf - finite, posInf); + EXPECT_EQ(posInf + finite, posInf); + EXPECT_EQ(negInf - finite, negInf); + EXPECT_EQ(negInf + finite, negInf); + + EXPECT_EQ(finite + posInf, posInf); + EXPECT_EQ(finite - posInf, negInf); + EXPECT_EQ(finite + negInf, negInf); + EXPECT_EQ(finite - negInf, posInf); +} diff --git a/dom/media/gtest/TestVPXDecoding.cpp b/dom/media/gtest/TestVPXDecoding.cpp new file mode 100644 index 0000000000..d58ca24cc7 --- /dev/null +++ b/dom/media/gtest/TestVPXDecoding.cpp @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "mozilla/ArrayUtils.h" +#include "nsTArray.h" +#include "VPXDecoder.h" + +#include <stdio.h> + +using namespace mozilla; + +static void ReadVPXFile(const char* aPath, nsTArray<uint8_t>& aBuffer) { + FILE* f = fopen(aPath, "rb"); + ASSERT_NE(f, (FILE*)nullptr); + + int r = fseek(f, 0, SEEK_END); + ASSERT_EQ(r, 0); + + long size = ftell(f); + ASSERT_NE(size, -1); + aBuffer.SetLength(size); + + r = fseek(f, 0, SEEK_SET); + ASSERT_EQ(r, 0); + + size_t got = fread(aBuffer.Elements(), 1, size, f); + ASSERT_EQ(got, size_t(size)); + + r = fclose(f); + ASSERT_EQ(r, 0); +} + +static vpx_codec_iface_t* ParseIVFConfig(nsTArray<uint8_t>& data, + vpx_codec_dec_cfg_t& config) { + if (data.Length() < 32 + 12) { + // Not enough data for file & first frame headers. + return nullptr; + } + if (data[0] != 'D' || data[1] != 'K' || data[2] != 'I' || data[3] != 'F') { + // Expect 'DKIP' + return nullptr; + } + if (data[4] != 0 || data[5] != 0) { + // Expect version==0. + return nullptr; + } + if (data[8] != 'V' || data[9] != 'P' || + (data[10] != '8' && data[10] != '9') || data[11] != '0') { + // Expect 'VP80' or 'VP90'. + return nullptr; + } + config.w = uint32_t(data[12]) || (uint32_t(data[13]) << 8); + config.h = uint32_t(data[14]) || (uint32_t(data[15]) << 8); + vpx_codec_iface_t* codec = + (data[10] == '8') ? vpx_codec_vp8_dx() : vpx_codec_vp9_dx(); + // Remove headers, to just leave raw VPx data to be decoded. + data.RemoveElementsAt(0, 32 + 12); + return codec; +} + +struct TestFileData { + const char* mFilename; + vpx_codec_err_t mDecodeResult; +}; +static const TestFileData testFiles[] = { + {"test_case_1224361.vp8.ivf", VPX_CODEC_OK}, + {"test_case_1224363.vp8.ivf", VPX_CODEC_CORRUPT_FRAME}, + {"test_case_1224369.vp8.ivf", VPX_CODEC_CORRUPT_FRAME}}; + +TEST(libvpx, test_cases) +{ + for (size_t test = 0; test < ArrayLength(testFiles); ++test) { + nsTArray<uint8_t> data; + ReadVPXFile(testFiles[test].mFilename, data); + ASSERT_GT(data.Length(), 0u); + + vpx_codec_dec_cfg_t config; + vpx_codec_iface_t* dx = ParseIVFConfig(data, config); + ASSERT_TRUE(dx); + config.threads = 2; + + vpx_codec_ctx_t ctx; + PodZero(&ctx); + vpx_codec_err_t r = vpx_codec_dec_init(&ctx, dx, &config, 0); + ASSERT_EQ(VPX_CODEC_OK, r); + + r = vpx_codec_decode(&ctx, data.Elements(), data.Length(), nullptr, 0); + // This test case is known to be corrupt. + EXPECT_EQ(testFiles[test].mDecodeResult, r); + + r = vpx_codec_destroy(&ctx); + EXPECT_EQ(VPX_CODEC_OK, r); + } +} diff --git a/dom/media/gtest/TestVideoFrameConverter.cpp b/dom/media/gtest/TestVideoFrameConverter.cpp new file mode 100644 index 0000000000..04ce47c473 --- /dev/null +++ b/dom/media/gtest/TestVideoFrameConverter.cpp @@ -0,0 +1,302 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "VideoFrameConverter.h" +#include "YUVBufferGenerator.h" + +using namespace mozilla; + +class VideoFrameConverterTest; + +class FrameListener : public VideoConverterListener { + public: + explicit FrameListener(VideoFrameConverterTest* aTest); + void OnVideoFrameConverted(const webrtc::VideoFrame& aVideoFrame) override; + + private: + VideoFrameConverterTest* mTest; +}; + +class VideoFrameConverterTest : public ::testing::Test { + protected: + using FrameType = std::pair<webrtc::VideoFrame, TimeStamp>; + Monitor mMonitor; + RefPtr<VideoFrameConverter> mConverter; + RefPtr<FrameListener> mListener; + std::vector<FrameType> mConvertedFrames; + + VideoFrameConverterTest() + : mMonitor("PacingFixture::mMonitor"), + mConverter(MakeAndAddRef<VideoFrameConverter>()), + mListener(MakeAndAddRef<FrameListener>(this)) { + mConverter->AddListener(mListener); + } + + void TearDown() override { mConverter->Shutdown(); } + + size_t NumConvertedFrames() { + MonitorAutoLock lock(mMonitor); + return mConvertedFrames.size(); + } + + std::vector<FrameType> WaitForNConverted(size_t aN) { + MonitorAutoLock l(mMonitor); + while (mConvertedFrames.size() < aN) { + l.Wait(); + } + std::vector<FrameType> v(mConvertedFrames.begin(), + mConvertedFrames.begin() + aN); + return v; + } + + public: + void OnVideoFrameConverted(const webrtc::VideoFrame& aVideoFrame) { + MonitorAutoLock lock(mMonitor); + EXPECT_NE(aVideoFrame.timestamp_us(), 0); + mConvertedFrames.push_back(std::make_pair(aVideoFrame, TimeStamp::Now())); + mMonitor.Notify(); + } +}; + +FrameListener::FrameListener(VideoFrameConverterTest* aTest) : mTest(aTest) {} +void FrameListener::OnVideoFrameConverted( + const webrtc::VideoFrame& aVideoFrame) { + mTest->OnVideoFrameConverted(aVideoFrame); +} + +static bool IsPlane(const uint8_t* aData, int aWidth, int aHeight, int aStride, + uint8_t aValue) { + for (int i = 0; i < aHeight; ++i) { + for (int j = 0; j < aWidth; ++j) { + if (aData[i * aStride + j] != aValue) { + return false; + } + } + } + return true; +} + +static bool IsFrameBlack(const webrtc::VideoFrame& aFrame) { + RefPtr<webrtc::I420BufferInterface> buffer = + aFrame.video_frame_buffer()->ToI420().get(); + return IsPlane(buffer->DataY(), buffer->width(), buffer->height(), + buffer->StrideY(), 0x00) && + IsPlane(buffer->DataU(), buffer->ChromaWidth(), buffer->ChromaHeight(), + buffer->StrideU(), 0x80) && + IsPlane(buffer->DataV(), buffer->ChromaWidth(), buffer->ChromaHeight(), + buffer->StrideV(), 0x80); +} + +VideoChunk GenerateChunk(int32_t aWidth, int32_t aHeight, TimeStamp aTime) { + YUVBufferGenerator generator; + generator.Init(gfx::IntSize(aWidth, aHeight)); + VideoFrame f(generator.GenerateI420Image(), gfx::IntSize(aWidth, aHeight)); + VideoChunk c; + c.mFrame.TakeFrom(&f); + c.mTimeStamp = aTime; + c.mDuration = 0; + return c; +} + +static TimeDuration SameFrameTimeDuration() { + // On some platforms, particularly Windows, we have observed the same-frame + // timer firing early. To not unittest the timer itself we allow a tiny amount + // of fuzziness in when the timer is allowed to fire. + return TimeDuration::FromSeconds(1) - TimeDuration::FromMilliseconds(0.5); +} + +TEST_F(VideoFrameConverterTest, BasicConversion) { + TimeStamp now = TimeStamp::Now(); + VideoChunk chunk = GenerateChunk(640, 480, now); + mConverter->SetActive(true); + mConverter->QueueVideoChunk(chunk, false); + auto frames = WaitForNConverted(1); + ASSERT_EQ(frames.size(), 1U); + EXPECT_EQ(frames[0].first.width(), 640); + EXPECT_EQ(frames[0].first.height(), 480); + EXPECT_FALSE(IsFrameBlack(frames[0].first)); + EXPECT_GT(frames[0].second - now, TimeDuration::FromMilliseconds(0)); +} + +TEST_F(VideoFrameConverterTest, BasicPacing) { + TimeStamp now = TimeStamp::Now(); + TimeStamp future = now + TimeDuration::FromMilliseconds(100); + VideoChunk chunk = GenerateChunk(640, 480, future); + mConverter->SetActive(true); + mConverter->QueueVideoChunk(chunk, false); + auto frames = WaitForNConverted(1); + EXPECT_GT(TimeStamp::Now(), future); + ASSERT_EQ(frames.size(), 1U); + EXPECT_EQ(frames[0].first.width(), 640); + EXPECT_EQ(frames[0].first.height(), 480); + EXPECT_FALSE(IsFrameBlack(frames[0].first)); + EXPECT_GT(frames[0].second - now, future - now); +} + +TEST_F(VideoFrameConverterTest, MultiPacing) { + TimeStamp now = TimeStamp::Now(); + TimeStamp future1 = now + TimeDuration::FromMilliseconds(100); + TimeStamp future2 = now + TimeDuration::FromMilliseconds(200); + VideoChunk chunk = GenerateChunk(640, 480, future1); + mConverter->SetActive(true); + mConverter->QueueVideoChunk(chunk, false); + chunk = GenerateChunk(640, 480, future2); + mConverter->QueueVideoChunk(chunk, false); + auto frames = WaitForNConverted(2); + EXPECT_GT(TimeStamp::Now(), future2); + ASSERT_EQ(frames.size(), 2U); + EXPECT_EQ(frames[0].first.width(), 640); + EXPECT_EQ(frames[0].first.height(), 480); + EXPECT_FALSE(IsFrameBlack(frames[0].first)); + EXPECT_GT(frames[0].second - now, future1 - now); + EXPECT_EQ(frames[1].first.width(), 640); + EXPECT_EQ(frames[1].first.height(), 480); + EXPECT_FALSE(IsFrameBlack(frames[1].first)); + EXPECT_GT(frames[1].second, future2); + EXPECT_GT(frames[1].second - now, frames[0].second - now); +} + +TEST_F(VideoFrameConverterTest, Duplication) { + TimeStamp now = TimeStamp::Now(); + TimeStamp future1 = now + TimeDuration::FromMilliseconds(100); + VideoChunk chunk = GenerateChunk(640, 480, future1); + mConverter->SetActive(true); + mConverter->QueueVideoChunk(chunk, false); + auto frames = WaitForNConverted(2); + EXPECT_GT(TimeStamp::Now() - now, + SameFrameTimeDuration() + TimeDuration::FromMilliseconds(100)); + ASSERT_EQ(frames.size(), 2U); + EXPECT_EQ(frames[0].first.width(), 640); + EXPECT_EQ(frames[0].first.height(), 480); + EXPECT_FALSE(IsFrameBlack(frames[0].first)); + EXPECT_GT(frames[0].second, future1); + EXPECT_EQ(frames[1].first.width(), 640); + EXPECT_EQ(frames[1].first.height(), 480); + EXPECT_FALSE(IsFrameBlack(frames[1].first)); + EXPECT_GT(frames[1].second - now, + SameFrameTimeDuration() + TimeDuration::FromMilliseconds(100)); + // Check that the second frame comes between 1s and 2s after the first. + EXPECT_GT(TimeDuration::FromMicroseconds(frames[1].first.timestamp_us()) - + TimeDuration::FromMicroseconds(frames[0].first.timestamp_us()), + SameFrameTimeDuration()); + EXPECT_LT(TimeDuration::FromMicroseconds(frames[1].first.timestamp_us()) - + TimeDuration::FromMicroseconds(frames[0].first.timestamp_us()), + TimeDuration::FromSeconds(2)); +} + +TEST_F(VideoFrameConverterTest, DropsOld) { + TimeStamp now = TimeStamp::Now(); + TimeStamp future1 = now + TimeDuration::FromMilliseconds(1000); + TimeStamp future2 = now + TimeDuration::FromMilliseconds(100); + mConverter->SetActive(true); + mConverter->QueueVideoChunk(GenerateChunk(800, 600, future1), false); + mConverter->QueueVideoChunk(GenerateChunk(640, 480, future2), false); + auto frames = WaitForNConverted(1); + EXPECT_GT(TimeStamp::Now(), future2); + ASSERT_EQ(frames.size(), 1U); + EXPECT_EQ(frames[0].first.width(), 640); + EXPECT_EQ(frames[0].first.height(), 480); + EXPECT_FALSE(IsFrameBlack(frames[0].first)); + EXPECT_GT(frames[0].second - now, future2 - now); +} + +// We check that the disabling code was triggered by sending multiple, +// different, frames to the converter within one second. While black, it shall +// treat all frames identical and issue one black frame per second. +TEST_F(VideoFrameConverterTest, BlackOnDisable) { + TimeStamp now = TimeStamp::Now(); + TimeStamp future1 = now + TimeDuration::FromMilliseconds(100); + TimeStamp future2 = now + TimeDuration::FromMilliseconds(200); + TimeStamp future3 = now + TimeDuration::FromMilliseconds(400); + mConverter->SetActive(true); + mConverter->SetTrackEnabled(false); + mConverter->QueueVideoChunk(GenerateChunk(640, 480, future1), false); + mConverter->QueueVideoChunk(GenerateChunk(640, 480, future2), false); + mConverter->QueueVideoChunk(GenerateChunk(640, 480, future3), false); + auto frames = WaitForNConverted(2); + EXPECT_GT(TimeStamp::Now() - now, SameFrameTimeDuration()); + ASSERT_EQ(frames.size(), 2U); + // The first frame was created instantly by SetTrackEnabled(). + EXPECT_EQ(frames[0].first.width(), 640); + EXPECT_EQ(frames[0].first.height(), 480); + EXPECT_TRUE(IsFrameBlack(frames[0].first)); + EXPECT_GT(frames[0].second - now, TimeDuration::FromSeconds(0)); + // The second frame was created by the same-frame timer (after 1s). + EXPECT_EQ(frames[1].first.width(), 640); + EXPECT_EQ(frames[1].first.height(), 480); + EXPECT_TRUE(IsFrameBlack(frames[1].first)); + EXPECT_GT(frames[1].second - now, SameFrameTimeDuration()); + // Check that the second frame comes between 1s and 2s after the first. + EXPECT_NEAR(frames[1].first.timestamp_us(), + frames[0].first.timestamp_us() + ((PR_USEC_PER_SEC * 3) / 2), + PR_USEC_PER_SEC / 2); +} + +TEST_F(VideoFrameConverterTest, ClearFutureFramesOnJumpingBack) { + TimeStamp start = TimeStamp::Now(); + TimeStamp future1 = start + TimeDuration::FromMilliseconds(100); + + mConverter->SetActive(true); + mConverter->QueueVideoChunk(GenerateChunk(640, 480, future1), false); + WaitForNConverted(1); + + // We are now at t=100ms+. Queue a future frame and jump back in time to + // signal a reset. + + TimeStamp step1 = TimeStamp::Now(); + ASSERT_GT(step1 - start, future1 - start); + TimeStamp future2 = step1 + TimeDuration::FromMilliseconds(200); + TimeStamp future3 = step1 + TimeDuration::FromMilliseconds(100); + ASSERT_LT(future2 - start, future1 + SameFrameTimeDuration() - start); + mConverter->QueueVideoChunk(GenerateChunk(800, 600, future2), false); + VideoChunk nullChunk; + nullChunk.mFrame = VideoFrame(nullptr, gfx::IntSize(800, 600)); + nullChunk.mTimeStamp = step1; + mConverter->QueueVideoChunk(nullChunk, false); + + // We queue one more chunk after the reset so we don't have to wait a full + // second for the same-frame timer. It has a different time and resolution + // so we can differentiate them. + mConverter->QueueVideoChunk(GenerateChunk(320, 240, future3), false); + + auto frames = WaitForNConverted(2); + TimeStamp step2 = TimeStamp::Now(); + EXPECT_GT(step2 - start, future3 - start); + ASSERT_EQ(frames.size(), 2U); + EXPECT_EQ(frames[0].first.width(), 640); + EXPECT_EQ(frames[0].first.height(), 480); + EXPECT_FALSE(IsFrameBlack(frames[0].first)); + EXPECT_GT(frames[0].second - start, future1 - start); + EXPECT_EQ(frames[1].first.width(), 320); + EXPECT_EQ(frames[1].first.height(), 240); + EXPECT_FALSE(IsFrameBlack(frames[1].first)); + EXPECT_GT(frames[1].second - start, future3 - start); +} + +// We check that the no frame is converted while inactive, and that on +// activating the most recently queued frame gets converted. +TEST_F(VideoFrameConverterTest, NoConversionsWhileInactive) { + TimeStamp now = TimeStamp::Now(); + TimeStamp future1 = now - TimeDuration::FromMilliseconds(1); + TimeStamp future2 = now; + mConverter->QueueVideoChunk(GenerateChunk(640, 480, future1), false); + mConverter->QueueVideoChunk(GenerateChunk(800, 600, future2), false); + + // SetActive needs to follow the same async path as the frames to be in sync. + auto q = + MakeRefPtr<TaskQueue>(GetMediaThreadPool(MediaThreadType::WEBRTC_DECODER), + "VideoFrameConverterTest"); + auto timer = MakeRefPtr<MediaTimer>(false); + timer->WaitFor(TimeDuration::FromMilliseconds(100), __func__) + ->Then(q, __func__, + [converter = mConverter] { converter->SetActive(true); }); + + auto frames = WaitForNConverted(1); + ASSERT_EQ(frames.size(), 1U); + EXPECT_EQ(frames[0].first.width(), 800); + EXPECT_EQ(frames[0].first.height(), 600); + EXPECT_FALSE(IsFrameBlack(frames[0].first)); +} diff --git a/dom/media/gtest/TestVideoSegment.cpp b/dom/media/gtest/TestVideoSegment.cpp new file mode 100644 index 0000000000..fd9f5ed285 --- /dev/null +++ b/dom/media/gtest/TestVideoSegment.cpp @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "VideoSegment.h" + +using namespace mozilla; + +namespace mozilla::layer { +class Image; +} // namespace mozilla::layer + +TEST(VideoSegment, TestAppendFrameForceBlack) +{ + RefPtr<layers::Image> testImage = nullptr; + + VideoSegment segment; + segment.AppendFrame(testImage.forget(), mozilla::gfx::IntSize(640, 480), + PRINCIPAL_HANDLE_NONE, true); + + VideoSegment::ChunkIterator iter(segment); + while (!iter.IsEnded()) { + VideoChunk chunk = *iter; + EXPECT_TRUE(chunk.mFrame.GetForceBlack()); + iter.Next(); + } +} + +TEST(VideoSegment, TestAppendFrameNotForceBlack) +{ + RefPtr<layers::Image> testImage = nullptr; + + VideoSegment segment; + segment.AppendFrame(testImage.forget(), mozilla::gfx::IntSize(640, 480), + PRINCIPAL_HANDLE_NONE); + + VideoSegment::ChunkIterator iter(segment); + while (!iter.IsEnded()) { + VideoChunk chunk = *iter; + EXPECT_FALSE(chunk.mFrame.GetForceBlack()); + iter.Next(); + } +} diff --git a/dom/media/gtest/TestVideoTrackEncoder.cpp b/dom/media/gtest/TestVideoTrackEncoder.cpp new file mode 100644 index 0000000000..087e709569 --- /dev/null +++ b/dom/media/gtest/TestVideoTrackEncoder.cpp @@ -0,0 +1,1569 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <algorithm> + +#include "DriftCompensation.h" +#include "MediaTrackGraph.h" +#include "MediaTrackListener.h" +#include "VP8TrackEncoder.h" +#include "WebMWriter.h" // TODO: it's weird to include muxer header to get the class definition of VP8 METADATA +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "mozilla/ArrayUtils.h" +#include "prtime.h" + +#include "YUVBufferGenerator.h" + +#define VIDEO_TRACK_RATE 90000 + +using ::testing::_; +using ::testing::Invoke; +using ::testing::NiceMock; +using ::testing::TestWithParam; +using ::testing::Values; + +using namespace mozilla::layers; +using namespace mozilla; + +struct InitParam { + bool mShouldSucceed; // This parameter should cause success or fail result + int mWidth; // frame width + int mHeight; // frame height +}; + +class MockDriftCompensator : public DriftCompensator { + public: + MockDriftCompensator() + : DriftCompensator(GetCurrentEventTarget(), VIDEO_TRACK_RATE) { + ON_CALL(*this, GetVideoTime(_, _)) + .WillByDefault(Invoke([](TimeStamp, TimeStamp t) { return t; })); + } + + MOCK_METHOD2(GetVideoTime, TimeStamp(TimeStamp, TimeStamp)); +}; + +class TestVP8TrackEncoder : public VP8TrackEncoder { + public: + explicit TestVP8TrackEncoder(TrackRate aTrackRate = VIDEO_TRACK_RATE) + : VP8TrackEncoder(MakeRefPtr<NiceMock<MockDriftCompensator>>(), + aTrackRate, FrameDroppingMode::DISALLOW) {} + + MockDriftCompensator* DriftCompensator() { + return static_cast<MockDriftCompensator*>(mDriftCompensator.get()); + } + + ::testing::AssertionResult TestInit(const InitParam& aParam) { + nsresult result = + Init(aParam.mWidth, aParam.mHeight, aParam.mWidth, aParam.mHeight); + + if (((NS_FAILED(result) && aParam.mShouldSucceed)) || + (NS_SUCCEEDED(result) && !aParam.mShouldSucceed)) { + return ::testing::AssertionFailure() + << " width = " << aParam.mWidth << " height = " << aParam.mHeight; + } + + return ::testing::AssertionSuccess(); + } +}; + +// Init test +TEST(VP8VideoTrackEncoder, Initialization) +{ + InitParam params[] = { + // Failure cases. + {false, 0, 0}, // Height/ width should be larger than 1. + {false, 0, 1}, // Height/ width should be larger than 1. + {false, 1, 0}, // Height/ width should be larger than 1. + + // Success cases + {true, 640, 480}, // Standard VGA + {true, 800, 480}, // Standard WVGA + {true, 960, 540}, // Standard qHD + {true, 1280, 720} // Standard HD + }; + + for (const InitParam& param : params) { + TestVP8TrackEncoder encoder; + EXPECT_TRUE(encoder.TestInit(param)); + } +} + +// Get MetaData test +TEST(VP8VideoTrackEncoder, FetchMetaData) +{ + InitParam params[] = { + // Success cases + {true, 640, 480}, // Standard VGA + {true, 800, 480}, // Standard WVGA + {true, 960, 540}, // Standard qHD + {true, 1280, 720} // Standard HD + }; + + for (const InitParam& param : params) { + TestVP8TrackEncoder encoder; + EXPECT_TRUE(encoder.TestInit(param)); + + RefPtr<TrackMetadataBase> meta = encoder.GetMetadata(); + RefPtr<VP8Metadata> vp8Meta(static_cast<VP8Metadata*>(meta.get())); + + // METADATA should be depend on how to initiate encoder. + EXPECT_EQ(vp8Meta->mWidth, param.mWidth); + EXPECT_EQ(vp8Meta->mHeight, param.mHeight); + } +} + +// Encode test +TEST(VP8VideoTrackEncoder, FrameEncode) +{ + TestVP8TrackEncoder encoder; + TimeStamp now = TimeStamp::Now(); + + // Create YUV images as source. + nsTArray<RefPtr<Image>> images; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + images.AppendElement(generator.GenerateI420Image()); + images.AppendElement(generator.GenerateNV12Image()); + images.AppendElement(generator.GenerateNV21Image()); + + // Put generated YUV frame into video segment. + // Duration of each frame is 1 second. + VideoSegment segment; + for (nsTArray<RefPtr<Image>>::size_type i = 0; i < images.Length(); i++) { + RefPtr<Image> image = images[i]; + segment.AppendFrame(image.forget(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromSeconds(i)); + } + + encoder.SetStartOffset(now); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(images.Length())); + + // Pull Encoded Data back from encoder. + nsTArray<RefPtr<EncodedFrame>> frames; + EXPECT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); +} + +// Test that encoding a single frame gives useful output. +TEST(VP8VideoTrackEncoder, SingleFrameEncode) +{ + TestVP8TrackEncoder encoder; + TimeStamp now = TimeStamp::Now(); + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + + // Pass a half-second frame to the encoder. + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + + encoder.SetStartOffset(now); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.5)); + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); + + // Read out encoded data, and verify. + const size_t oneElement = 1; + ASSERT_EQ(oneElement, frames.Length()); + + EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frames[0]->mFrameType) + << "We only have one frame, so it should be a keyframe"; + + const uint64_t halfSecond = PR_USEC_PER_SEC / 2; + EXPECT_EQ(halfSecond, frames[0]->mDuration); +} + +// Test that encoding a couple of identical images gives useful output. +TEST(VP8VideoTrackEncoder, SameFrameEncode) +{ + TestVP8TrackEncoder encoder; + TimeStamp now = TimeStamp::Now(); + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + + // Pass 15 100ms frames to the encoder. + RefPtr<Image> image = generator.GenerateI420Image(); + VideoSegment segment; + for (uint32_t i = 0; i < 15; ++i) { + segment.AppendFrame(do_AddRef(image), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromSeconds(i * 0.1)); + } + + encoder.SetStartOffset(now); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(1.5)); + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); + + // Verify total duration being 1.5s. + uint64_t totalDuration = 0; + for (auto& frame : frames) { + totalDuration += frame->mDuration; + } + const uint64_t oneAndAHalf = (PR_USEC_PER_SEC / 2) * 3; + EXPECT_EQ(oneAndAHalf, totalDuration); +} + +// Test encoding a track that has to skip frames. +TEST(VP8VideoTrackEncoder, SkippedFrames) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + TimeStamp now = TimeStamp::Now(); + + // Pass 100 frames of the shortest possible duration where we don't get + // rounding errors between input/output rate. + VideoSegment segment; + for (uint32_t i = 0; i < 100; ++i) { + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(i)); + } + + encoder.SetStartOffset(now); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(100)); + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); + + // Verify total duration being 100 * 1ms = 100ms. + uint64_t totalDuration = 0; + for (auto& frame : frames) { + totalDuration += frame->mDuration; + } + const uint64_t hundredMillis = PR_USEC_PER_SEC / 10; + EXPECT_EQ(hundredMillis, totalDuration); +} + +// Test encoding a track with frames subject to rounding errors. +TEST(VP8VideoTrackEncoder, RoundingErrorFramesEncode) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + TimeStamp now = TimeStamp::Now(); + + // Pass nine frames with timestamps not expressable in 90kHz sample rate, + // then one frame to make the total duration one second. + VideoSegment segment; + uint32_t usPerFrame = 99999; // 99.999ms + for (uint32_t i = 0; i < 9; ++i) { + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMicroseconds(i * usPerFrame)); + } + + // This last frame has timestamp start + 0.9s and duration 0.1s. + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromSeconds(0.9)); + + encoder.SetStartOffset(now); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(1)); + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); + + // Verify total duration being 1s. + uint64_t totalDuration = 0; + for (auto& frame : frames) { + totalDuration += frame->mDuration; + } + const uint64_t oneSecond = PR_USEC_PER_SEC; + EXPECT_EQ(oneSecond, totalDuration); +} + +// Test that we're encoding timestamps rather than durations. +TEST(VP8VideoTrackEncoder, TimestampFrameEncode) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + TimeStamp now = TimeStamp::Now(); + + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromSeconds(0.05)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromSeconds(0.2)); + + encoder.SetStartOffset(now); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.3)); + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); + + // Verify total duration being 0.3s and individual frames being [0.05s, 0.15s, + // 0.1s] + uint64_t expectedDurations[] = {(PR_USEC_PER_SEC / 10) / 2, + (PR_USEC_PER_SEC / 10) * 3 / 2, + (PR_USEC_PER_SEC / 10)}; + uint64_t totalDuration = 0; + size_t i = 0; + for (auto& frame : frames) { + EXPECT_EQ(expectedDurations[i], frame->mDuration); + i++; + totalDuration += frame->mDuration; + } + const uint64_t pointThree = (PR_USEC_PER_SEC / 10) * 3; + EXPECT_EQ(pointThree, totalDuration); +} + +// Test that we're compensating for drift when encoding. +TEST(VP8VideoTrackEncoder, DriftingFrameEncode) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + TimeStamp now = TimeStamp::Now(); + + // Set up major drift -- audio that goes twice as fast as video. + // This should make the given video durations double as they get encoded. + EXPECT_CALL(*encoder.DriftCompensator(), GetVideoTime(_, _)) + .WillRepeatedly(Invoke( + [&](TimeStamp, TimeStamp aTime) { return now + (aTime - now) * 2; })); + + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromSeconds(0.05)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromSeconds(0.2)); + + encoder.SetStartOffset(now); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.3)); + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); + + // Verify total duration being 0.6s and individual frames being [0.1s, 0.3s, + // 0.2s] + uint64_t expectedDurations[] = {(PR_USEC_PER_SEC / 10), + (PR_USEC_PER_SEC / 10) * 3, + (PR_USEC_PER_SEC / 10) * 2}; + uint64_t totalDuration = 0; + size_t i = 0; + for (auto& frame : frames) { + EXPECT_EQ(expectedDurations[i], frame->mDuration); + i++; + totalDuration += frame->mDuration; + } + const uint64_t pointSix = (PR_USEC_PER_SEC / 10) * 6; + EXPECT_EQ(pointSix, totalDuration); +} + +// Test that suspending an encoding works. +TEST(VP8VideoTrackEncoder, Suspended) +{ + TestVP8TrackEncoder encoder; + TimeStamp now = TimeStamp::Now(); + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + + // Pass 3 frames with duration 0.1s. We suspend before and resume after the + // second frame. + + { + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + + encoder.SetStartOffset(now); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.1)); + } + + encoder.Suspend(now + TimeDuration::FromSeconds(0.1)); + + { + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromSeconds(0.1)); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.2)); + } + + encoder.Resume(now + TimeDuration::FromSeconds(0.2)); + + { + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromSeconds(0.2)); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.3)); + } + + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); + + // Verify that we have two encoded frames and a total duration of 0.2s. + const uint64_t two = 2; + EXPECT_EQ(two, frames.Length()); + + uint64_t totalDuration = 0; + for (auto& frame : frames) { + totalDuration += frame->mDuration; + } + const uint64_t pointTwo = (PR_USEC_PER_SEC / 10) * 2; + EXPECT_EQ(pointTwo, totalDuration); +} + +// Test that ending a track while the video track encoder is suspended works. +TEST(VP8VideoTrackEncoder, SuspendedUntilEnd) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + TimeStamp now = TimeStamp::Now(); + + // Pass 2 frames with duration 0.1s. We suspend before the second frame. + + { + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + + encoder.SetStartOffset(now); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.1)); + } + + encoder.Suspend(now + TimeDuration::FromSeconds(0.1)); + + { + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromSeconds(0.1)); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.2)); + } + + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); + + // Verify that we have one encoded frames and a total duration of 0.1s. + const uint64_t one = 1; + EXPECT_EQ(one, frames.Length()); + + uint64_t totalDuration = 0; + for (auto& frame : frames) { + totalDuration += frame->mDuration; + } + const uint64_t pointOne = PR_USEC_PER_SEC / 10; + EXPECT_EQ(pointOne, totalDuration); +} + +// Test that ending a track that was always suspended works. +TEST(VP8VideoTrackEncoder, AlwaysSuspended) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + TimeStamp now = TimeStamp::Now(); + + // Suspend and then pass a frame with duration 2s. + + encoder.Suspend(now); + + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + + encoder.SetStartOffset(now); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(2)); + + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); + + // Verify that we have no encoded frames. + const uint64_t none = 0; + EXPECT_EQ(none, frames.Length()); +} + +// Test that encoding a track that is suspended in the beginning works. +TEST(VP8VideoTrackEncoder, SuspendedBeginning) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + TimeStamp now = TimeStamp::Now(); + + // Suspend and pass a frame with duration 0.5s. Then resume and pass one more. + encoder.Suspend(now); + + { + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + + encoder.SetStartOffset(now); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.5)); + } + + encoder.Resume(now + TimeDuration::FromSeconds(0.5)); + + { + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromSeconds(0.5)); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(1)); + } + + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); + + // Verify that we have one encoded frames and a total duration of 0.1s. + const uint64_t one = 1; + EXPECT_EQ(one, frames.Length()); + + uint64_t totalDuration = 0; + for (auto& frame : frames) { + totalDuration += frame->mDuration; + } + const uint64_t half = PR_USEC_PER_SEC / 2; + EXPECT_EQ(half, totalDuration); +} + +// Test that suspending and resuming in the middle of already pushed data +// works. +TEST(VP8VideoTrackEncoder, SuspendedOverlap) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + TimeStamp now = TimeStamp::Now(); + + { + // Pass a 1s frame and suspend after 0.5s. + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + + encoder.SetStartOffset(now); + encoder.AppendVideoSegment(std::move(segment)); + } + + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.5)); + encoder.Suspend(now + TimeDuration::FromSeconds(0.5)); + + { + // Pass another 1s frame and resume after 0.3 of this new frame. + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromSeconds(1)); + encoder.AppendVideoSegment(std::move(segment)); + } + + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(1.3)); + encoder.Resume(now + TimeDuration::FromSeconds(1.3)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(2)); + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); + + // Verify that we have two encoded frames and a total duration of 0.1s. + const uint64_t two = 2; + ASSERT_EQ(two, frames.Length()); + const uint64_t pointFive = (PR_USEC_PER_SEC / 10) * 5; + EXPECT_EQ(pointFive, frames[0]->mDuration); + const uint64_t pointSeven = (PR_USEC_PER_SEC / 10) * 7; + EXPECT_EQ(pointSeven, frames[1]->mDuration); +} + +// Test that ending a track in the middle of already pushed data works. +TEST(VP8VideoTrackEncoder, PrematureEnding) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + TimeStamp now = TimeStamp::Now(); + + // Pass a 1s frame and end the track after 0.5s. + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + + encoder.SetStartOffset(now); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.5)); + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); + + uint64_t totalDuration = 0; + for (auto& frame : frames) { + totalDuration += frame->mDuration; + } + const uint64_t half = PR_USEC_PER_SEC / 2; + EXPECT_EQ(half, totalDuration); +} + +// Test that a track that starts at t > 0 works as expected. +TEST(VP8VideoTrackEncoder, DelayedStart) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + TimeStamp now = TimeStamp::Now(); + + // Pass a 2s frame, start (pass first CurrentTime) at 0.5s, end at 1s. + // Should result in a 0.5s encoding. + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + + encoder.SetStartOffset(now + TimeDuration::FromSeconds(0.5)); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(1)); + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); + + uint64_t totalDuration = 0; + for (auto& frame : frames) { + totalDuration += frame->mDuration; + } + const uint64_t half = PR_USEC_PER_SEC / 2; + EXPECT_EQ(half, totalDuration); +} + +// Test that a track that starts at t > 0 works as expected, when +// SetStartOffset comes after AppendVideoSegment. +TEST(VP8VideoTrackEncoder, DelayedStartOtherEventOrder) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + TimeStamp now = TimeStamp::Now(); + + // Pass a 2s frame, start (pass first CurrentTime) at 0.5s, end at 1s. + // Should result in a 0.5s encoding. + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + + encoder.AppendVideoSegment(std::move(segment)); + encoder.SetStartOffset(now + TimeDuration::FromSeconds(0.5)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(1)); + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); + + uint64_t totalDuration = 0; + for (auto& frame : frames) { + totalDuration += frame->mDuration; + } + const uint64_t half = PR_USEC_PER_SEC / 2; + EXPECT_EQ(half, totalDuration); +} + +// Test that a track that starts at t >>> 0 works as expected. +TEST(VP8VideoTrackEncoder, VeryDelayedStart) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + TimeStamp now = TimeStamp::Now(); + + // Pass a 1s frame, start (pass first CurrentTime) at 10s, end at 10.5s. + // Should result in a 0.5s encoding. + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + + encoder.SetStartOffset(now + TimeDuration::FromSeconds(10)); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(10.5)); + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); + + uint64_t totalDuration = 0; + for (auto& frame : frames) { + totalDuration += frame->mDuration; + } + const uint64_t half = PR_USEC_PER_SEC / 2; + EXPECT_EQ(half, totalDuration); +} + +// Test that a video frame that hangs around for a long time gets encoded every +// second. +TEST(VP8VideoTrackEncoder, LongFramesReEncoded) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + TimeStamp now = TimeStamp::Now(); + + // Pass a frame at t=0 and start encoding. + // Advancing the current time by 1.5s should encode a 1s frame. + // Advancing the current time by another 9.5s should encode another 10 1s + // frames. + + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + + encoder.SetStartOffset(now); + encoder.AppendVideoSegment(std::move(segment)); + + { + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(1.5)); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + EXPECT_FALSE(encoder.IsEncodingComplete()); + + uint64_t totalDuration = 0; + for (auto& frame : frames) { + totalDuration += frame->mDuration; + } + const uint64_t oneSec = PR_USEC_PER_SEC; + EXPECT_EQ(oneSec, totalDuration); + EXPECT_EQ(1U, frames.Length()); + } + + { + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(11)); + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + EXPECT_TRUE(encoder.IsEncodingComplete()); + + uint64_t totalDuration = 0; + for (auto& frame : frames) { + totalDuration += frame->mDuration; + } + const uint64_t tenSec = PR_USEC_PER_SEC * 10; + EXPECT_EQ(tenSec, totalDuration); + EXPECT_EQ(10U, frames.Length()); + } +} + +// Test that an encoding with a defined key frame interval encodes keyframes +// as expected. Short here means shorter than the default (1s). +TEST(VP8VideoTrackEncoder, ShortKeyFrameInterval) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + TimeStamp now = TimeStamp::Now(); + + // Give the encoder a keyframe interval of 500ms. + // Pass frames at 0, 400ms, 600ms, 750ms, 900ms, 1100ms + // Expected keys: ^ ^^^^^ ^^^^^^ + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(400)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(600)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(750)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(900)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(1100)); + + encoder.SetKeyFrameInterval(500); + encoder.SetStartOffset(now); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(1.2)); + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); + + ASSERT_EQ(6UL, frames.Length()); + + // [0, 400ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 400UL, frames[0]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frames[0]->mFrameType); + + // [400ms, 600ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 200UL, frames[1]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_P_FRAME, frames[1]->mFrameType); + + // [600ms, 750ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 150UL, frames[2]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frames[2]->mFrameType); + + // [750ms, 900ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 150UL, frames[3]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_P_FRAME, frames[3]->mFrameType); + + // [900ms, 1100ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 200UL, frames[4]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_P_FRAME, frames[4]->mFrameType); + + // [1100ms, 1200ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[5]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frames[5]->mFrameType); +} + +// Test that an encoding with a defined key frame interval encodes keyframes +// as expected. Long here means longer than the default (1s). +TEST(VP8VideoTrackEncoder, LongKeyFrameInterval) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + TimeStamp now = TimeStamp::Now(); + + // Give the encoder a keyframe interval of 2000ms. + // Pass frames at 0, 600ms, 900ms, 1100ms, 1900ms, 2100ms + // Expected keys: ^ ^^^^^^ ^^^^^^ + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(600)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(900)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(1100)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(1900)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(2100)); + + encoder.SetKeyFrameInterval(2000); + encoder.SetStartOffset(now); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(2.2)); + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); + + ASSERT_EQ(6UL, frames.Length()); + + // [0, 600ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 600UL, frames[0]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frames[0]->mFrameType); + + // [600ms, 900ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 300UL, frames[1]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_P_FRAME, frames[1]->mFrameType); + + // [900ms, 1100ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 200UL, frames[2]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_P_FRAME, frames[2]->mFrameType); + + // [1100ms, 1900ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 800UL, frames[3]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frames[3]->mFrameType); + + // [1900ms, 2100ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 200UL, frames[4]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_P_FRAME, frames[4]->mFrameType); + + // [2100ms, 2200ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[5]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frames[5]->mFrameType); +} + +// Test that an encoding with no defined key frame interval encodes keyframes +// as expected. Default interval should be 1000ms. +TEST(VP8VideoTrackEncoder, DefaultKeyFrameInterval) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + TimeStamp now = TimeStamp::Now(); + + // Pass frames at 0, 600ms, 900ms, 1100ms, 1900ms, 2100ms + // Expected keys: ^ ^^^^^^ ^^^^^^ + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(600)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(900)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(1100)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(1900)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(2100)); + + encoder.SetStartOffset(now); + encoder.AppendVideoSegment(std::move(segment)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(2.2)); + encoder.NotifyEndOfStream(); + + nsTArray<RefPtr<EncodedFrame>> frames; + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); + + ASSERT_EQ(6UL, frames.Length()); + + // [0, 600ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 600UL, frames[0]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frames[0]->mFrameType); + + // [600ms, 900ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 300UL, frames[1]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_P_FRAME, frames[1]->mFrameType); + + // [900ms, 1100ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 200UL, frames[2]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_P_FRAME, frames[2]->mFrameType); + + // [1100ms, 1900ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 800UL, frames[3]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frames[3]->mFrameType); + + // [1900ms, 2100ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 200UL, frames[4]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_P_FRAME, frames[4]->mFrameType); + + // [2100ms, 2200ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[5]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frames[5]->mFrameType); +} + +// Test that an encoding where the key frame interval is updated dynamically +// encodes keyframes as expected. +TEST(VP8VideoTrackEncoder, DynamicKeyFrameIntervalChanges) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + nsTArray<RefPtr<EncodedFrame>> frames; + TimeStamp now = TimeStamp::Now(); + + // Set keyframe interval to 100ms. + // Pass frames at 0, 100ms, 120ms, 130ms, 200ms, 300ms + // Expected keys: ^ ^^^^^ ^^^^^ ^^^^^ + + // Then increase keyframe interval to 1100ms. (default is 1000) + // Pass frames at 500ms, 1300ms, 1400ms, 2400ms + // Expected keys: ^^^^^^ ^^^^^^ + + // Then decrease keyframe interval to 200ms. + // Pass frames at 2500ms, 2600ms, 2800ms, 2900ms + // Expected keys: ^^^^^^ ^^^^^^ + + { + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(100)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(120)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(130)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(200)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(300)); + + // The underlying encoder only gets passed frame N when frame N+1 is known, + // so we pass in the next frame *before* the keyframe interval change. + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(500)); + + encoder.SetStartOffset(now); + encoder.SetKeyFrameInterval(100); + encoder.AppendVideoSegment(std::move(segment)); + } + + // Advancing 501ms, so the first bit of the frame starting at 500ms is + // included. + encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(501)); + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + { + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(1300)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(1400)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(2400)); + + // The underlying encoder only gets passed frame N when frame N+1 is known, + // so we pass in the next frame *before* the keyframe interval change. + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(2500)); + + encoder.SetKeyFrameInterval(1100); + encoder.AppendVideoSegment(std::move(segment)); + } + + // Advancing 2000ms from 501ms to 2501ms + encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(2501)); + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + { + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(2600)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(2800)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(2900)); + + encoder.SetKeyFrameInterval(200); + encoder.AppendVideoSegment(std::move(segment)); + } + + // Advancing 499ms (compensating back 1ms from the first advancement) + // from 2501ms to 3000ms. + encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(3000)); + + encoder.NotifyEndOfStream(); + + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); + + ASSERT_EQ(14UL, frames.Length()); + + // [0, 100ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[0]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frames[0]->mFrameType); + + // [100ms, 120ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 20UL, frames[1]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frames[1]->mFrameType); + + // [120ms, 130ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 10UL, frames[2]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_P_FRAME, frames[2]->mFrameType); + + // [130ms, 200ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 70UL, frames[3]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_P_FRAME, frames[3]->mFrameType); + + // [200ms, 300ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[4]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frames[4]->mFrameType); + + // [300ms, 500ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 200UL, frames[5]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frames[5]->mFrameType); + + // [500ms, 1300ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 800UL, frames[6]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_P_FRAME, frames[6]->mFrameType); + + // [1300ms, 1400ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[7]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frames[7]->mFrameType); + + // [1400ms, 2400ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 1000UL, frames[8]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_P_FRAME, frames[8]->mFrameType); + + // [2400ms, 2500ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[9]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frames[9]->mFrameType); + + // [2500ms, 2600ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[10]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_P_FRAME, frames[10]->mFrameType); + + // [2600ms, 2800ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 200UL, frames[11]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frames[11]->mFrameType); + + // [2800ms, 2900ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[12]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frames[12]->mFrameType); + + // [2900ms, 3000ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[13]->mDuration); + EXPECT_EQ(EncodedFrame::VP8_P_FRAME, frames[13]->mFrameType); +} + +// Test that an encoding which is disabled on a frame timestamp encodes +// frames as expected. +TEST(VP8VideoTrackEncoder, DisableOnFrameTime) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + nsTArray<RefPtr<EncodedFrame>> frames; + TimeStamp now = TimeStamp::Now(); + + // Pass a frame in at t=0. + // Pass another frame in at t=100ms. + // Disable the track at t=100ms. + // Stop encoding at t=200ms. + // Should yield 2 frames, 1 real; 1 black. + + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(100)); + + encoder.SetStartOffset(now); + encoder.AppendVideoSegment(std::move(segment)); + + // Advancing 100ms, for simplicity. + encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(100)); + + encoder.Disable(now + TimeDuration::FromMilliseconds(100)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(200)); + encoder.NotifyEndOfStream(); + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + EXPECT_TRUE(encoder.IsEncodingComplete()); + + ASSERT_EQ(2UL, frames.Length()); + + // [0, 100ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[0]->mDuration); + + // [100ms, 200ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[1]->mDuration); +} + +// Test that an encoding which is disabled between two frame timestamps encodes +// frames as expected. +TEST(VP8VideoTrackEncoder, DisableBetweenFrames) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + nsTArray<RefPtr<EncodedFrame>> frames; + TimeStamp now = TimeStamp::Now(); + + // Pass a frame in at t=0. + // Disable the track at t=50ms. + // Pass another frame in at t=100ms. + // Stop encoding at t=200ms. + // Should yield 3 frames, 1 real [0, 50); 2 black [50, 100) and [100, 200). + + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(100)); + + encoder.SetStartOffset(now); + encoder.AppendVideoSegment(std::move(segment)); + + encoder.Disable(now + TimeDuration::FromMilliseconds(50)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(200)); + encoder.NotifyEndOfStream(); + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + EXPECT_TRUE(encoder.IsEncodingComplete()); + + ASSERT_EQ(3UL, frames.Length()); + + // [0, 50ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 50UL, frames[0]->mDuration); + + // [50ms, 100ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 50UL, frames[1]->mDuration); + + // [100ms, 200ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[2]->mDuration); +} + +// Test that an encoding which is disabled before the first frame becomes black +// immediately. +TEST(VP8VideoTrackEncoder, DisableBeforeFirstFrame) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + nsTArray<RefPtr<EncodedFrame>> frames; + TimeStamp now = TimeStamp::Now(); + + // Disable the track at t=0. + // Pass a frame in at t=50ms. + // Enable the track at t=100ms. + // Stop encoding at t=200ms. + // Should yield 2 frames, 1 black [0, 100); 1 real [100, 200). + + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(50)); + + encoder.SetStartOffset(now); + encoder.Disable(now); + encoder.AppendVideoSegment(std::move(segment)); + + encoder.Enable(now + TimeDuration::FromMilliseconds(100)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(200)); + encoder.NotifyEndOfStream(); + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + EXPECT_TRUE(encoder.IsEncodingComplete()); + + ASSERT_EQ(2UL, frames.Length()); + + // [0, 100ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[0]->mDuration); + + // [100ms, 200ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[1]->mDuration); +} + +// Test that an encoding which is enabled on a frame timestamp encodes +// frames as expected. +TEST(VP8VideoTrackEncoder, EnableOnFrameTime) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + nsTArray<RefPtr<EncodedFrame>> frames; + TimeStamp now = TimeStamp::Now(); + + // Disable the track at t=0. + // Pass a frame in at t=0. + // Pass another frame in at t=100ms. + // Enable the track at t=100ms. + // Stop encoding at t=200ms. + // Should yield 2 frames, 1 black; 1 real. + + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(100)); + + encoder.SetStartOffset(now); + encoder.Disable(now); + encoder.AppendVideoSegment(std::move(segment)); + + // Advancing 100ms, for simplicity. + encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(100)); + + encoder.Enable(now + TimeDuration::FromMilliseconds(100)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(200)); + encoder.NotifyEndOfStream(); + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + EXPECT_TRUE(encoder.IsEncodingComplete()); + + ASSERT_EQ(2UL, frames.Length()); + + // [0, 100ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[0]->mDuration); + + // [100ms, 200ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[1]->mDuration); +} + +// Test that an encoding which is enabled between two frame timestamps encodes +// frames as expected. +TEST(VP8VideoTrackEncoder, EnableBetweenFrames) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + nsTArray<RefPtr<EncodedFrame>> frames; + TimeStamp now = TimeStamp::Now(); + + // Disable the track at t=0. + // Pass a frame in at t=0. + // Enable the track at t=50ms. + // Pass another frame in at t=100ms. + // Stop encoding at t=200ms. + // Should yield 3 frames, 1 black [0, 50); 2 real [50, 100) and [100, 200). + + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(100)); + + encoder.SetStartOffset(now); + encoder.Disable(now); + encoder.AppendVideoSegment(std::move(segment)); + + encoder.Enable(now + TimeDuration::FromMilliseconds(50)); + encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(200)); + encoder.NotifyEndOfStream(); + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + EXPECT_TRUE(encoder.IsEncodingComplete()); + + ASSERT_EQ(3UL, frames.Length()); + + // [0, 50ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 50UL, frames[0]->mDuration); + + // [50ms, 100ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 50UL, frames[1]->mDuration); + + // [100ms, 200ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[2]->mDuration); +} + +// Test that making time go backwards removes any future frames in the encoder. +TEST(VP8VideoTrackEncoder, BackwardsTimeResets) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + nsTArray<RefPtr<EncodedFrame>> frames; + TimeStamp now = TimeStamp::Now(); + + encoder.SetStartOffset(now); + + // Pass frames in at t=0, t=100ms, t=200ms, t=300ms. + // Advance time to t=125ms. + // Pass frames in at t=150ms, t=250ms, t=350ms. + // Stop encoding at t=300ms. + // Should yield 4 frames, at t=0, t=100ms, t=150ms, t=250ms. + + { + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(100)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(200)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(300)); + + encoder.AppendVideoSegment(std::move(segment)); + } + + encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(125)); + + { + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(150)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(250)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(350)); + + encoder.AppendVideoSegment(std::move(segment)); + } + + encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(300)); + encoder.NotifyEndOfStream(); + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + EXPECT_TRUE(encoder.IsEncodingComplete()); + + ASSERT_EQ(4UL, frames.Length()); + + // [0, 100ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[0]->mDuration); + + // [100ms, 150ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 50UL, frames[1]->mDuration); + + // [150ms, 250ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[2]->mDuration); + + // [250ms, 300ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 50UL, frames[3]->mDuration); +} + +// Test that trying to encode a null image removes any future frames in the +// encoder. +TEST(VP8VideoTrackEncoder, NullImageResets) +{ + TestVP8TrackEncoder encoder; + YUVBufferGenerator generator; + generator.Init(mozilla::gfx::IntSize(640, 480)); + nsTArray<RefPtr<EncodedFrame>> frames; + TimeStamp now = TimeStamp::Now(); + + encoder.SetStartOffset(now); + + // Pass frames in at t=0, t=100ms, t=200ms, t=300ms. + // Advance time to t=125ms. + // Pass in a null image at t=125ms. + // Pass frames in at t=250ms, t=350ms. + // Stop encoding at t=300ms. + // Should yield 3 frames, at t=0, t=100ms, t=250ms. + + { + VideoSegment segment; + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, now); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(100)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(200)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(300)); + + encoder.AppendVideoSegment(std::move(segment)); + } + + encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(125)); + + { + VideoSegment segment; + segment.AppendFrame(nullptr, generator.GetSize(), PRINCIPAL_HANDLE_NONE, + false, now + TimeDuration::FromMilliseconds(125)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(250)); + segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(), + PRINCIPAL_HANDLE_NONE, false, + now + TimeDuration::FromMilliseconds(350)); + + encoder.AppendVideoSegment(std::move(segment)); + } + + encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(300)); + encoder.NotifyEndOfStream(); + ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + EXPECT_TRUE(encoder.IsEncodingComplete()); + + ASSERT_EQ(3UL, frames.Length()); + + // [0, 100ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frames[0]->mDuration); + + // [100ms, 250ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 150UL, frames[1]->mDuration); + + // [250ms, 300ms) + EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 50UL, frames[2]->mDuration); +} + +// EOS test +TEST(VP8VideoTrackEncoder, EncodeComplete) +{ + TestVP8TrackEncoder encoder; + + // track end notification. + encoder.NotifyEndOfStream(); + + // Pull Encoded Data back from encoder. Since we have sent + // EOS to encoder, encoder.GetEncodedTrack should return + // NS_OK immidiately. + nsTArray<RefPtr<EncodedFrame>> frames; + EXPECT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(frames))); + + EXPECT_TRUE(encoder.IsEncodingComplete()); +} diff --git a/dom/media/gtest/TestVideoUtils.cpp b/dom/media/gtest/TestVideoUtils.cpp new file mode 100644 index 0000000000..d322d15d64 --- /dev/null +++ b/dom/media/gtest/TestVideoUtils.cpp @@ -0,0 +1,128 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "nsMimeTypes.h" +#include "nsString.h" +#include "VideoUtils.h" + +using namespace mozilla; + +TEST(MediaMIMETypes, IsMediaMIMEType) +{ + EXPECT_TRUE(IsMediaMIMEType(AUDIO_MP4)); + EXPECT_TRUE(IsMediaMIMEType(VIDEO_MP4)); + EXPECT_TRUE(IsMediaMIMEType("application/x-mp4")); + + EXPECT_TRUE(IsMediaMIMEType("audio/m")); + EXPECT_FALSE(IsMediaMIMEType("audio/")); + + EXPECT_FALSE(IsMediaMIMEType("vide/mp4")); + EXPECT_FALSE(IsMediaMIMEType("videos/mp4")); + + // Expect lowercase only. + EXPECT_FALSE(IsMediaMIMEType("Video/mp4")); +} + +TEST(StringListRange, MakeStringListRange) +{ + static const struct { + const char* mList; + const char* mExpectedSkipEmpties; + const char* mExpectedProcessAll; + const char* mExpectedProcessEmpties; + } tests[] = { + // string skip all empties + {"", "", "|", ""}, + {" ", "", "|", "|"}, + {",", "", "||", "||"}, + {" , ", "", "||", "||"}, + {"a", "a|", "a|", "a|"}, + {" a ", "a|", "a|", "a|"}, + {"a,", "a|", "a||", "a||"}, + {"a, ", "a|", "a||", "a||"}, + {",a", "a|", "|a|", "|a|"}, + {" ,a", "a|", "|a|", "|a|"}, + {"aa,bb", "aa|bb|", "aa|bb|", "aa|bb|"}, + {" a a , b b ", "a a|b b|", "a a|b b|", "a a|b b|"}, + {" , ,a 1,, ,b 2,", "a 1|b 2|", "||a 1|||b 2||", "||a 1|||b 2||"}}; + + for (const auto& test : tests) { + nsCString list(test.mList); + nsCString out; + for (const auto& item : MakeStringListRange(list)) { + out += item; + out += "|"; + } + EXPECT_STREQ(test.mExpectedSkipEmpties, out.Data()); + out.SetLength(0); + + for (const auto& item : + MakeStringListRange<StringListRangeEmptyItems::ProcessAll>(list)) { + out += item; + out += "|"; + } + EXPECT_STREQ(test.mExpectedProcessAll, out.Data()); + out.SetLength(0); + + for (const auto& item : + MakeStringListRange<StringListRangeEmptyItems::ProcessEmptyItems>( + list)) { + out += item; + out += "|"; + } + EXPECT_STREQ(test.mExpectedProcessEmpties, out.Data()); + } +} + +TEST(StringListRange, StringListContains) +{ + static const struct { + const char* mList; + const char* mItemToSearch; + bool mExpectedSkipEmpties; + bool mExpectedProcessAll; + bool mExpectedProcessEmpties; + } tests[] = {// haystack needle skip all empties + {"", "", false, true, false}, + {" ", "", false, true, true}, + {"", "a", false, false, false}, + {" ", "a", false, false, false}, + {",", "a", false, false, false}, + {" , ", "", false, true, true}, + {" , ", "a", false, false, false}, + {"a", "a", true, true, true}, + {"a", "b", false, false, false}, + {" a ", "a", true, true, true}, + {"aa,bb", "aa", true, true, true}, + {"aa,bb", "bb", true, true, true}, + {"aa,bb", "cc", false, false, false}, + {"aa,bb", " aa ", false, false, false}, + {" a a , b b ", "a a", true, true, true}, + {" , ,a 1,, ,b 2,", "a 1", true, true, true}, + {" , ,a 1,, ,b 2,", "b 2", true, true, true}, + {" , ,a 1,, ,b 2,", "", false, true, true}, + {" , ,a 1,, ,b 2,", " ", false, false, false}, + {" , ,a 1,, ,b 2,", "A 1", false, false, false}, + {" , ,A 1,, ,b 2,", "a 1", false, false, false}}; + + for (const auto& test : tests) { + nsCString list(test.mList); + nsCString itemToSearch(test.mItemToSearch); + EXPECT_EQ(test.mExpectedSkipEmpties, StringListContains(list, itemToSearch)) + << "trying to find \"" << itemToSearch.Data() << "\" in \"" + << list.Data() << "\" (skipping empties)"; + EXPECT_EQ(test.mExpectedProcessAll, + StringListContains<StringListRangeEmptyItems::ProcessAll>( + list, itemToSearch)) + << "trying to find \"" << itemToSearch.Data() << "\" in \"" + << list.Data() << "\" (processing everything)"; + EXPECT_EQ(test.mExpectedProcessEmpties, + StringListContains<StringListRangeEmptyItems::ProcessEmptyItems>( + list, itemToSearch)) + << "trying to find \"" << itemToSearch.Data() << "\" in \"" + << list.Data() << "\" (processing empties)"; + } +} diff --git a/dom/media/gtest/TestWebMBuffered.cpp b/dom/media/gtest/TestWebMBuffered.cpp new file mode 100644 index 0000000000..14f5ae2b25 --- /dev/null +++ b/dom/media/gtest/TestWebMBuffered.cpp @@ -0,0 +1,118 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "mozilla/ArrayUtils.h" +#include <stdio.h> +#include "nsTArray.h" +#include "WebMBufferedParser.h" + +using namespace mozilla; + +// "test.webm" contains 8 SimpleBlocks in a single Cluster. The blocks with +// timecodes 100000000 and are 133000000 skipped by WebMBufferedParser +// because they occur after a block with timecode 160000000 and the parser +// expects in-order timecodes per the WebM spec. The remaining 6 +// SimpleBlocks have the following attributes: +static const uint64_t gTimecodes[] = {66000000, 160000000, 166000000, + 200000000, 233000000, 320000000}; +static const int64_t gEndOffsets[] = {501, 772, 1244, 1380, 1543, 2015}; + +TEST(WebMBuffered, BasicTests) +{ + ReentrantMonitor dummy("dummy"); + WebMBufferedParser parser(0); + + nsTArray<WebMTimeDataOffset> mapping; + parser.Append(nullptr, 0, mapping, dummy); + EXPECT_TRUE(mapping.IsEmpty()); + EXPECT_EQ(parser.mStartOffset, 0); + EXPECT_EQ(parser.mCurrentOffset, 0); + + unsigned char buf[] = {0x1a, 0x45, 0xdf, 0xa3}; + parser.Append(buf, ArrayLength(buf), mapping, dummy); + EXPECT_TRUE(mapping.IsEmpty()); + EXPECT_EQ(parser.mStartOffset, 0); + EXPECT_EQ(parser.mCurrentOffset, 4); +} + +static void ReadFile(const char* aPath, nsTArray<uint8_t>& aBuffer) { + FILE* f = fopen(aPath, "rb"); + ASSERT_NE(f, (FILE*)nullptr); + + int r = fseek(f, 0, SEEK_END); + ASSERT_EQ(r, 0); + + long size = ftell(f); + ASSERT_NE(size, -1); + aBuffer.SetLength(size); + + r = fseek(f, 0, SEEK_SET); + ASSERT_EQ(r, 0); + + size_t got = fread(aBuffer.Elements(), 1, size, f); + ASSERT_EQ(got, size_t(size)); + + r = fclose(f); + ASSERT_EQ(r, 0); +} + +TEST(WebMBuffered, RealData) +{ + ReentrantMonitor dummy("dummy"); + WebMBufferedParser parser(0); + + nsTArray<uint8_t> webmData; + ReadFile("test.webm", webmData); + + nsTArray<WebMTimeDataOffset> mapping; + parser.Append(webmData.Elements(), webmData.Length(), mapping, dummy); + EXPECT_EQ(mapping.Length(), 6u); + EXPECT_EQ(parser.mStartOffset, 0); + EXPECT_EQ(parser.mCurrentOffset, int64_t(webmData.Length())); + EXPECT_EQ(parser.GetTimecodeScale(), 500000u); + + for (uint32_t i = 0; i < mapping.Length(); ++i) { + EXPECT_EQ(mapping[i].mEndOffset, gEndOffsets[i]); + EXPECT_EQ(mapping[i].mSyncOffset, 361); + EXPECT_EQ(mapping[i].mTimecode, gTimecodes[i]); + } +} + +TEST(WebMBuffered, RealDataAppend) +{ + ReentrantMonitor dummy("dummy"); + WebMBufferedParser parser(0); + nsTArray<WebMTimeDataOffset> mapping; + + nsTArray<uint8_t> webmData; + ReadFile("test.webm", webmData); + + uint32_t arrayEntries = mapping.Length(); + size_t offset = 0; + while (offset < webmData.Length()) { + parser.Append(webmData.Elements() + offset, 1, mapping, dummy); + offset += 1; + EXPECT_EQ(parser.mCurrentOffset, int64_t(offset)); + if (mapping.Length() != arrayEntries) { + arrayEntries = mapping.Length(); + ASSERT_LE(arrayEntries, 6u); + uint32_t i = arrayEntries - 1; + EXPECT_EQ(mapping[i].mEndOffset, gEndOffsets[i]); + EXPECT_EQ(mapping[i].mSyncOffset, 361); + EXPECT_EQ(mapping[i].mTimecode, gTimecodes[i]); + EXPECT_EQ(parser.GetTimecodeScale(), 500000u); + } + } + EXPECT_EQ(mapping.Length(), 6u); + EXPECT_EQ(parser.mStartOffset, 0); + EXPECT_EQ(parser.mCurrentOffset, int64_t(webmData.Length())); + EXPECT_EQ(parser.GetTimecodeScale(), 500000u); + + for (uint32_t i = 0; i < mapping.Length(); ++i) { + EXPECT_EQ(mapping[i].mEndOffset, gEndOffsets[i]); + EXPECT_EQ(mapping[i].mSyncOffset, 361); + EXPECT_EQ(mapping[i].mTimecode, gTimecodes[i]); + } +} diff --git a/dom/media/gtest/TestWebMWriter.cpp b/dom/media/gtest/TestWebMWriter.cpp new file mode 100644 index 0000000000..176c9f0808 --- /dev/null +++ b/dom/media/gtest/TestWebMWriter.cpp @@ -0,0 +1,358 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/MathAlgorithms.h" +#include "nestegg/nestegg.h" +#include "DriftCompensation.h" +#include "OpusTrackEncoder.h" +#include "VP8TrackEncoder.h" +#include "WebMWriter.h" + +using namespace mozilla; + +class WebMOpusTrackEncoder : public OpusTrackEncoder { + public: + explicit WebMOpusTrackEncoder(TrackRate aTrackRate) + : OpusTrackEncoder(aTrackRate) {} + bool TestOpusCreation(int aChannels) { + if (NS_SUCCEEDED(Init(aChannels))) { + return true; + } + return false; + } +}; + +class WebMVP8TrackEncoder : public VP8TrackEncoder { + public: + explicit WebMVP8TrackEncoder(TrackRate aTrackRate = 90000) + : VP8TrackEncoder(nullptr, aTrackRate, FrameDroppingMode::DISALLOW) {} + + bool TestVP8Creation(int32_t aWidth, int32_t aHeight, int32_t aDisplayWidth, + int32_t aDisplayHeight) { + if (NS_SUCCEEDED(Init(aWidth, aHeight, aDisplayWidth, aDisplayHeight))) { + return true; + } + return false; + } +}; + +static void GetOpusMetadata(int aChannels, TrackRate aTrackRate, + nsTArray<RefPtr<TrackMetadataBase>>& aMeta) { + WebMOpusTrackEncoder opusEncoder(aTrackRate); + EXPECT_TRUE(opusEncoder.TestOpusCreation(aChannels)); + aMeta.AppendElement(opusEncoder.GetMetadata()); +} + +static void GetVP8Metadata(int32_t aWidth, int32_t aHeight, + int32_t aDisplayWidth, int32_t aDisplayHeight, + TrackRate aTrackRate, + nsTArray<RefPtr<TrackMetadataBase>>& aMeta) { + WebMVP8TrackEncoder vp8Encoder; + EXPECT_TRUE(vp8Encoder.TestVP8Creation(aWidth, aHeight, aDisplayWidth, + aDisplayHeight)); + aMeta.AppendElement(vp8Encoder.GetMetadata()); +} + +const uint64_t FIXED_DURATION = 1000000; +const uint32_t FIXED_FRAMESIZE = 500; + +class TestWebMWriter : public WebMWriter { + public: + TestWebMWriter() : WebMWriter() {} + + // When we append an I-Frame into WebM muxer, the muxer will treat previous + // data as "a cluster". + // In these test cases, we will call the function many times to enclose the + // previous cluster so that we can retrieve data by |GetContainerData|. + void AppendDummyFrame(EncodedFrame::FrameType aFrameType, + uint64_t aDuration) { + nsTArray<RefPtr<EncodedFrame>> encodedVideoData; + auto frameData = MakeRefPtr<EncodedFrame::FrameData>(); + // Create dummy frame data. + frameData->SetLength(FIXED_FRAMESIZE); + encodedVideoData.AppendElement( + MakeRefPtr<EncodedFrame>(mTimestamp, aDuration, PR_USEC_PER_SEC, + aFrameType, std::move(frameData))); + WriteEncodedTrack(encodedVideoData, 0); + mTimestamp += media::TimeUnit::FromMicroseconds(aDuration); + } + + bool HaveValidCluster() { + nsTArray<nsTArray<uint8_t>> encodedBuf; + GetContainerData(&encodedBuf, 0); + return (encodedBuf.Length() > 0) ? true : false; + } + + // Timestamp accumulator that increased by AppendDummyFrame. + // Keep it public that we can do some testcases about it. + media::TimeUnit mTimestamp; +}; + +TEST(WebMWriter, Metadata) +{ + TestWebMWriter writer; + + // The output should be empty since we didn't set any metadata in writer. + nsTArray<nsTArray<uint8_t>> encodedBuf; + writer.GetContainerData(&encodedBuf, ContainerWriter::GET_HEADER); + EXPECT_TRUE(encodedBuf.Length() == 0); + writer.GetContainerData(&encodedBuf, ContainerWriter::FLUSH_NEEDED); + EXPECT_TRUE(encodedBuf.Length() == 0); + + nsTArray<RefPtr<TrackMetadataBase>> meta; + + TrackRate trackRate = 44100; + + // Get opus metadata. + int channel = 1; + GetOpusMetadata(channel, trackRate, meta); + + // Get vp8 metadata + int32_t width = 640; + int32_t height = 480; + int32_t displayWidth = 640; + int32_t displayHeight = 480; + GetVP8Metadata(width, height, displayWidth, displayHeight, trackRate, meta); + + // Set metadata + writer.SetMetadata(meta); + + writer.GetContainerData(&encodedBuf, ContainerWriter::GET_HEADER); + EXPECT_TRUE(encodedBuf.Length() > 0); +} + +TEST(WebMWriter, Cluster) +{ + TestWebMWriter writer; + nsTArray<RefPtr<TrackMetadataBase>> meta; + TrackRate trackRate = 48000; + // Get opus metadata. + int channel = 1; + GetOpusMetadata(channel, trackRate, meta); + // Get vp8 metadata + int32_t width = 320; + int32_t height = 240; + int32_t displayWidth = 320; + int32_t displayHeight = 240; + GetVP8Metadata(width, height, displayWidth, displayHeight, trackRate, meta); + writer.SetMetadata(meta); + + nsTArray<nsTArray<uint8_t>> encodedBuf; + writer.GetContainerData(&encodedBuf, ContainerWriter::GET_HEADER); + EXPECT_TRUE(encodedBuf.Length() > 0); + encodedBuf.Clear(); + + // write the first I-Frame. + writer.AppendDummyFrame(EncodedFrame::VP8_I_FRAME, FIXED_DURATION); + // No data because the cluster is not closed. + EXPECT_FALSE(writer.HaveValidCluster()); + + // The second I-Frame. + writer.AppendDummyFrame(EncodedFrame::VP8_I_FRAME, FIXED_DURATION); + // Should have data because the first cluster is closed. + EXPECT_TRUE(writer.HaveValidCluster()); + + // P-Frame. + writer.AppendDummyFrame(EncodedFrame::VP8_P_FRAME, FIXED_DURATION); + // No data because the cluster is not closed. + EXPECT_FALSE(writer.HaveValidCluster()); + + // The third I-Frame. + writer.AppendDummyFrame(EncodedFrame::VP8_I_FRAME, FIXED_DURATION); + // Should have data because the second cluster is closed. + EXPECT_TRUE(writer.HaveValidCluster()); +} + +TEST(WebMWriter, FLUSH_NEEDED) +{ + TestWebMWriter writer; + nsTArray<RefPtr<TrackMetadataBase>> meta; + TrackRate trackRate = 44100; + // Get opus metadata. + int channel = 2; + GetOpusMetadata(channel, trackRate, meta); + // Get vp8 metadata + int32_t width = 176; + int32_t height = 352; + int32_t displayWidth = 176; + int32_t displayHeight = 352; + GetVP8Metadata(width, height, displayWidth, displayHeight, trackRate, meta); + writer.SetMetadata(meta); + + // write the first I-Frame. + writer.AppendDummyFrame(EncodedFrame::VP8_I_FRAME, FIXED_DURATION); + + // P-Frame + writer.AppendDummyFrame(EncodedFrame::VP8_P_FRAME, FIXED_DURATION); + // Have data because the metadata is finished. + EXPECT_TRUE(writer.HaveValidCluster()); + // No data because the cluster is not closed and the metatdata had been + // retrieved + EXPECT_FALSE(writer.HaveValidCluster()); + + nsTArray<nsTArray<uint8_t>> encodedBuf; + // Have data because the flag ContainerWriter::FLUSH_NEEDED + writer.GetContainerData(&encodedBuf, ContainerWriter::FLUSH_NEEDED); + EXPECT_TRUE(encodedBuf.Length() > 0); + encodedBuf.Clear(); + + // P-Frame + writer.AppendDummyFrame(EncodedFrame::VP8_P_FRAME, FIXED_DURATION); + // No data because there is no cluster right now. The I-Frame had been + // flushed out. + EXPECT_FALSE(writer.HaveValidCluster()); + + // I-Frame + writer.AppendDummyFrame(EncodedFrame::VP8_I_FRAME, FIXED_DURATION); + // No data because a cluster must starts form I-Frame and the + // cluster is not closed. + EXPECT_FALSE(writer.HaveValidCluster()); + + // I-Frame + writer.AppendDummyFrame(EncodedFrame::VP8_I_FRAME, FIXED_DURATION); + // Have data because the previous cluster is closed. + EXPECT_TRUE(writer.HaveValidCluster()); +} + +struct WebMioData { + nsTArray<uint8_t> data; + CheckedInt<size_t> offset; +}; + +static int webm_read(void* aBuffer, size_t aLength, void* aUserData) { + NS_ASSERTION(aUserData, "aUserData must point to a valid WebMioData"); + WebMioData* ioData = static_cast<WebMioData*>(aUserData); + + // Check the read length. + if (aLength > ioData->data.Length()) { + return 0; + } + + // Check eos. + if (ioData->offset.value() >= ioData->data.Length()) { + return 0; + } + + size_t oldOffset = ioData->offset.value(); + ioData->offset += aLength; + if (!ioData->offset.isValid() || + (ioData->offset.value() > ioData->data.Length())) { + return -1; + } + memcpy(aBuffer, ioData->data.Elements() + oldOffset, aLength); + return 1; +} + +static int webm_seek(int64_t aOffset, int aWhence, void* aUserData) { + NS_ASSERTION(aUserData, "aUserData must point to a valid WebMioData"); + WebMioData* ioData = static_cast<WebMioData*>(aUserData); + + if (Abs(aOffset) > ioData->data.Length()) { + NS_ERROR("Invalid aOffset"); + return -1; + } + + switch (aWhence) { + case NESTEGG_SEEK_END: { + CheckedInt<size_t> tempOffset = ioData->data.Length(); + ioData->offset = tempOffset + aOffset; + break; + } + case NESTEGG_SEEK_CUR: + ioData->offset += aOffset; + break; + case NESTEGG_SEEK_SET: + ioData->offset = aOffset; + break; + default: + NS_ERROR("Unknown whence"); + return -1; + } + + if (!ioData->offset.isValid()) { + NS_ERROR("Invalid offset"); + return -1; + } + + return 0; +} + +static int64_t webm_tell(void* aUserData) { + NS_ASSERTION(aUserData, "aUserData must point to a valid WebMioData"); + WebMioData* ioData = static_cast<WebMioData*>(aUserData); + return ioData->offset.isValid() ? ioData->offset.value() : -1; +} + +TEST(WebMWriter, bug970774_aspect_ratio) +{ + TestWebMWriter writer; + nsTArray<RefPtr<TrackMetadataBase>> meta; + TrackRate trackRate = 44100; + // Get opus metadata. + int channel = 1; + GetOpusMetadata(channel, trackRate, meta); + // Set vp8 metadata + int32_t width = 640; + int32_t height = 480; + int32_t displayWidth = 1280; + int32_t displayHeight = 960; + GetVP8Metadata(width, height, displayWidth, displayHeight, trackRate, meta); + writer.SetMetadata(meta); + + // write the first I-Frame. + writer.AppendDummyFrame(EncodedFrame::VP8_I_FRAME, FIXED_DURATION); + + // write the second I-Frame. + writer.AppendDummyFrame(EncodedFrame::VP8_I_FRAME, FIXED_DURATION); + + // Get the metadata and the first cluster. + nsTArray<nsTArray<uint8_t>> encodedBuf; + writer.GetContainerData(&encodedBuf, 0); + // Flatten the encodedBuf. + WebMioData ioData; + ioData.offset = 0; + for (uint32_t i = 0; i < encodedBuf.Length(); ++i) { + ioData.data.AppendElements(encodedBuf[i]); + } + + // Use nestegg to verify the information in metadata. + nestegg* context = nullptr; + nestegg_io io; + io.read = webm_read; + io.seek = webm_seek; + io.tell = webm_tell; + io.userdata = static_cast<void*>(&ioData); + int rv = nestegg_init(&context, io, nullptr, -1); + EXPECT_EQ(rv, 0); + unsigned int ntracks = 0; + rv = nestegg_track_count(context, &ntracks); + EXPECT_EQ(rv, 0); + EXPECT_EQ(ntracks, (unsigned int)2); + for (unsigned int track = 0; track < ntracks; ++track) { + int id = nestegg_track_codec_id(context, track); + EXPECT_NE(id, -1); + int type = nestegg_track_type(context, track); + if (type == NESTEGG_TRACK_VIDEO) { + nestegg_video_params params; + rv = nestegg_track_video_params(context, track, ¶ms); + EXPECT_EQ(rv, 0); + EXPECT_EQ(width, static_cast<int32_t>(params.width)); + EXPECT_EQ(height, static_cast<int32_t>(params.height)); + EXPECT_EQ(displayWidth, static_cast<int32_t>(params.display_width)); + EXPECT_EQ(displayHeight, static_cast<int32_t>(params.display_height)); + } else if (type == NESTEGG_TRACK_AUDIO) { + nestegg_audio_params params; + rv = nestegg_track_audio_params(context, track, ¶ms); + EXPECT_EQ(rv, 0); + EXPECT_EQ(channel, static_cast<int>(params.channels)); + EXPECT_EQ(static_cast<double>(trackRate), params.rate); + } + } + if (context) { + nestegg_destroy(context); + } +} diff --git a/dom/media/gtest/WaitFor.h b/dom/media/gtest/WaitFor.h new file mode 100644 index 0000000000..d8299e7a77 --- /dev/null +++ b/dom/media/gtest/WaitFor.h @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef WAITFOR_H_ +#define WAITFOR_H_ + +#include "MediaEventSource.h" +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "mozilla/SpinEventLoopUntil.h" + +namespace mozilla { + +/** + * Waits for an occurrence of aEvent on the current thread (by blocking it, + * except tasks added to the event loop may run) and returns the event's + * templated value, if it's non-void. + * + * The caller must be wary of eventloop issues, in + * particular cases where we rely on a stable state runnable, but there is never + * a task to trigger stable state. In such cases it is the responsibility of the + * caller to create the needed tasks, as JS would. A noteworthy API that relies + * on stable state is MediaTrackGraph::GetInstance. + */ +template <typename T> +T WaitFor(MediaEventSource<T>& aEvent) { + Maybe<T> value; + MediaEventListener listener = aEvent.Connect( + AbstractThread::GetCurrent(), [&](T aValue) { value = Some(aValue); }); + SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>( + [&] { return value.isSome(); }); + listener.Disconnect(); + return value.value(); +} + +/** + * Specialization of WaitFor<T> for void. + */ +void WaitFor(MediaEventSource<void>& aEvent) { + bool done = false; + MediaEventListener listener = + aEvent.Connect(AbstractThread::GetCurrent(), [&] { done = true; }); + SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>( + [&] { return done; }); + listener.Disconnect(); +} + +/** + * Variant of WaitFor that blocks the caller until a MozPromise has either been + * resolved or rejected. + */ +template <typename R, typename E, bool Exc> +Result<R, E> WaitFor(const RefPtr<MozPromise<R, E, Exc>>& aPromise) { + Maybe<R> success; + Maybe<E> error; + aPromise->Then( + GetCurrentSerialEventTarget(), __func__, + [&](R aResult) { success = Some(aResult); }, + [&](E aError) { error = Some(aError); }); + SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>( + [&] { return success.isSome() || error.isSome(); }); + if (success.isSome()) { + return success.extract(); + } + return Err(error.extract()); +} + +/** + * A variation of WaitFor that takes a callback to be called each time aEvent is + * raised. Blocks the caller until the callback function returns true. + */ +template <typename T, typename CallbackFunction> +void WaitUntil(MediaEventSource<T>& aEvent, const CallbackFunction& aF) { + bool done = false; + MediaEventListener listener = + aEvent.Connect(AbstractThread::GetCurrent(), [&](T aValue) { + if (!done) { + done = aF(aValue); + } + }); + SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>( + [&] { return done; }); + listener.Disconnect(); +} + +} // namespace mozilla + +#endif // WAITFOR_H_ diff --git a/dom/media/gtest/YUVBufferGenerator.cpp b/dom/media/gtest/YUVBufferGenerator.cpp new file mode 100644 index 0000000000..b3a4772fcf --- /dev/null +++ b/dom/media/gtest/YUVBufferGenerator.cpp @@ -0,0 +1,157 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "YUVBufferGenerator.h" + +#include "VideoUtils.h" + +using namespace mozilla::layers; +using namespace mozilla; + +void YUVBufferGenerator::Init(const mozilla::gfx::IntSize& aSize) { + mImageSize = aSize; + + int yPlaneLen = aSize.width * aSize.height; + int cbcrPlaneLen = (yPlaneLen + 1) / 2; + int frameLen = yPlaneLen + cbcrPlaneLen; + + // Generate source buffer. + mSourceBuffer.SetLength(frameLen); + + // Fill Y plane. + memset(mSourceBuffer.Elements(), 0x10, yPlaneLen); + + // Fill Cb/Cr planes. + memset(mSourceBuffer.Elements() + yPlaneLen, 0x80, cbcrPlaneLen); +} + +mozilla::gfx::IntSize YUVBufferGenerator::GetSize() const { return mImageSize; } + +already_AddRefed<Image> YUVBufferGenerator::GenerateI420Image() { + return do_AddRef(CreateI420Image()); +} + +already_AddRefed<Image> YUVBufferGenerator::GenerateNV12Image() { + return do_AddRef(CreateNV12Image()); +} + +already_AddRefed<Image> YUVBufferGenerator::GenerateNV21Image() { + return do_AddRef(CreateNV21Image()); +} + +Image* YUVBufferGenerator::CreateI420Image() { + PlanarYCbCrImage* image = + new RecyclingPlanarYCbCrImage(new BufferRecycleBin()); + PlanarYCbCrData data; + data.mPicSize = mImageSize; + + const uint32_t yPlaneSize = mImageSize.width * mImageSize.height; + const uint32_t halfWidth = (mImageSize.width + 1) / 2; + const uint32_t halfHeight = (mImageSize.height + 1) / 2; + const uint32_t uvPlaneSize = halfWidth * halfHeight; + + // Y plane. + uint8_t* y = mSourceBuffer.Elements(); + data.mYChannel = y; + data.mYSize.width = mImageSize.width; + data.mYSize.height = mImageSize.height; + data.mYStride = mImageSize.width; + data.mYSkip = 0; + + // Cr plane. + uint8_t* cr = y + yPlaneSize + uvPlaneSize; + data.mCrChannel = cr; + data.mCrSkip = 0; + + // Cb plane + uint8_t* cb = y + yPlaneSize; + data.mCbChannel = cb; + data.mCbSkip = 0; + + // CrCb plane vectors. + data.mCbCrStride = halfWidth; + data.mCbCrSize.width = halfWidth; + data.mCbCrSize.height = halfHeight; + + data.mYUVColorSpace = DefaultColorSpace(data.mYSize); + + image->CopyData(data); + return image; +} + +Image* YUVBufferGenerator::CreateNV12Image() { + NVImage* image = new NVImage(); + PlanarYCbCrData data; + data.mPicSize = mImageSize; + + const uint32_t yPlaneSize = mImageSize.width * mImageSize.height; + const uint32_t halfWidth = (mImageSize.width + 1) / 2; + const uint32_t halfHeight = (mImageSize.height + 1) / 2; + + // Y plane. + uint8_t* y = mSourceBuffer.Elements(); + data.mYChannel = y; + data.mYSize.width = mImageSize.width; + data.mYSize.height = mImageSize.height; + data.mYStride = mImageSize.width; + data.mYSkip = 0; + + // Cr plane. + uint8_t* cr = y + yPlaneSize; + data.mCrChannel = cr; + data.mCrSkip = 1; + + // Cb plane + uint8_t* cb = y + yPlaneSize + 1; + data.mCbChannel = cb; + data.mCbSkip = 1; + + // 4:2:0. + data.mCbCrStride = mImageSize.width; + data.mCbCrSize.width = halfWidth; + data.mCbCrSize.height = halfHeight; + + image->SetData(data); + return image; +} + +Image* YUVBufferGenerator::CreateNV21Image() { + NVImage* image = new NVImage(); + PlanarYCbCrData data; + data.mPicSize = mImageSize; + + const uint32_t yPlaneSize = mImageSize.width * mImageSize.height; + const uint32_t halfWidth = (mImageSize.width + 1) / 2; + const uint32_t halfHeight = (mImageSize.height + 1) / 2; + + // Y plane. + uint8_t* y = mSourceBuffer.Elements(); + data.mYChannel = y; + data.mYSize.width = mImageSize.width; + data.mYSize.height = mImageSize.height; + data.mYStride = mImageSize.width; + data.mYSkip = 0; + + // Cr plane. + uint8_t* cr = y + yPlaneSize + 1; + data.mCrChannel = cr; + data.mCrSkip = 1; + + // Cb plane + uint8_t* cb = y + yPlaneSize; + data.mCbChannel = cb; + data.mCbSkip = 1; + + // 4:2:0. + data.mCbCrStride = mImageSize.width; + data.mCbCrSize.width = halfWidth; + data.mCbCrSize.height = halfHeight; + + data.mYUVColorSpace = DefaultColorSpace(data.mYSize); + + image->SetData(data); + return image; +} diff --git a/dom/media/gtest/YUVBufferGenerator.h b/dom/media/gtest/YUVBufferGenerator.h new file mode 100644 index 0000000000..cb6ed6b220 --- /dev/null +++ b/dom/media/gtest/YUVBufferGenerator.h @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef YUVBufferGenerator_h +#define YUVBufferGenerator_h + +#include "ImageContainer.h" +#include "mozilla/AlreadyAddRefed.h" +#include "nsTArray.h" +#include "Point.h" // mozilla::gfx::IntSize + +// A helper object to generate of different YUV planes. +class YUVBufferGenerator { + public: + void Init(const mozilla::gfx::IntSize& aSize); + mozilla::gfx::IntSize GetSize() const; + already_AddRefed<mozilla::layers::Image> GenerateI420Image(); + already_AddRefed<mozilla::layers::Image> GenerateNV12Image(); + already_AddRefed<mozilla::layers::Image> GenerateNV21Image(); + + private: + mozilla::layers::Image* CreateI420Image(); + mozilla::layers::Image* CreateNV12Image(); + mozilla::layers::Image* CreateNV21Image(); + mozilla::gfx::IntSize mImageSize; + nsTArray<uint8_t> mSourceBuffer; +}; + +#endif // YUVBufferGenerator_h diff --git a/dom/media/gtest/dash_dashinit.mp4 b/dom/media/gtest/dash_dashinit.mp4 Binary files differnew file mode 100644 index 0000000000..d19068f36d --- /dev/null +++ b/dom/media/gtest/dash_dashinit.mp4 diff --git a/dom/media/gtest/hello.rs b/dom/media/gtest/hello.rs new file mode 100644 index 0000000000..af1308eee6 --- /dev/null +++ b/dom/media/gtest/hello.rs @@ -0,0 +1,6 @@ +#[no_mangle] +pub extern "C" fn test_rust() -> *const u8 { + // NB: rust &str aren't null terminated. + let greeting = "hello from rust.\0"; + greeting.as_ptr() +} diff --git a/dom/media/gtest/id3v2header.mp3 b/dom/media/gtest/id3v2header.mp3 Binary files differnew file mode 100644 index 0000000000..2f5585d02e --- /dev/null +++ b/dom/media/gtest/id3v2header.mp3 diff --git a/dom/media/gtest/moz.build b/dom/media/gtest/moz.build new file mode 100644 index 0000000000..7810b1e16f --- /dev/null +++ b/dom/media/gtest/moz.build @@ -0,0 +1,129 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +include("/dom/media/webrtc/third_party_build/webrtc.mozbuild") + +DEFINES["ENABLE_SET_CUBEB_BACKEND"] = True + +LOCAL_INCLUDES += [ + "/dom/media/webrtc/common/", + "/third_party/libwebrtc", + "/third_party/libwebrtc/webrtc", +] + +UNIFIED_SOURCES += [ + "MockCubeb.cpp", + "MockMediaResource.cpp", + "TestAudioBuffers.cpp", + "TestAudioCallbackDriver.cpp", + "TestAudioCompactor.cpp", + "TestAudioDriftCorrection.cpp", + "TestAudioMixer.cpp", + "TestAudioPacketizer.cpp", + "TestAudioRingBuffer.cpp", + "TestAudioSegment.cpp", + "TestAudioTrackEncoder.cpp", + "TestAudioTrackGraph.cpp", + "TestBenchmarkStorage.cpp", + "TestBitWriter.cpp", + "TestBlankVideoDataCreator.cpp", + "TestBufferReader.cpp", + "TestDataMutex.cpp", + "TestDecoderBenchmark.cpp", + "TestDriftCompensation.cpp", + "TestDynamicResampler.cpp", + "TestGMPUtils.cpp", + "TestGroupId.cpp", + "TestIntervalSet.cpp", + "TestKeyValueStorage.cpp", + "TestMediaDataDecoder.cpp", + "TestMediaDataEncoder.cpp", + "TestMediaEventSource.cpp", + "TestMediaMIMETypes.cpp", + "TestMediaSpan.cpp", + "TestMP3Demuxer.cpp", + "TestMP4Demuxer.cpp", + "TestMuxer.cpp", + "TestOpusParser.cpp", + "TestRust.cpp", + "TestTimeUnit.cpp", + "TestVideoSegment.cpp", + "TestVideoUtils.cpp", + "TestVPXDecoding.cpp", + "TestWebMBuffered.cpp", +] + +if CONFIG["MOZ_WEBM_ENCODER"]: + UNIFIED_SOURCES += [ + "TestVideoTrackEncoder.cpp", + "TestWebMWriter.cpp", + "YUVBufferGenerator.cpp", + ] + LOCAL_INCLUDES += [ + "/gfx/2d/", + ] + +if CONFIG["OS_TARGET"] != "Android": + UNIFIED_SOURCES += [ + "TestCDMStorage.cpp", + "TestGMPCrossOrigin.cpp", + "TestGMPRemoveAndDelete.cpp", + ] + +if CONFIG["MOZ_WEBRTC"] and CONFIG["OS_TARGET"] != "Android": + UNIFIED_SOURCES += [ + "TestAudioDeviceEnumerator.cpp", + "TestVideoFrameConverter.cpp", + ] + +TEST_HARNESS_FILES.gtest += [ + "../test/gizmo-frag.mp4", + "../test/gizmo.mp4", + "../test/vp9cake.webm", + "dash_dashinit.mp4", + "id3v2header.mp3", + "negative_duration.mp4", + "noise.mp3", + "noise_vbr.mp3", + "short-zero-in-moov.mp4", + "short-zero-inband.mov", + "small-shot-false-positive.mp3", + "small-shot-partial-xing.mp3", + "small-shot.mp3", + "test.webm", + "test_case_1224361.vp8.ivf", + "test_case_1224363.vp8.ivf", + "test_case_1224369.vp8.ivf", + "test_vbri.mp3", +] + +TEST_DIRS += [ + "mp4_demuxer", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +LOCAL_INCLUDES += [ + "/dom/media", + "/dom/media/encoder", + "/dom/media/gmp", + "/dom/media/mp4", + "/dom/media/platforms", + "/dom/media/platforms/agnostic", + "/dom/media/webrtc", + "/security/certverifier", +] + +FINAL_LIBRARY = "xul-gtest" + +if CONFIG["CC_TYPE"] in ("clang", "gcc"): + CXXFLAGS += ["-Wno-error=shadow"] + +if CONFIG["CC_TYPE"] in ("clang", "clang-cl"): + CXXFLAGS += [ + "-Wno-inconsistent-missing-override", + "-Wno-unused-private-field", + ] diff --git a/dom/media/gtest/mp4_demuxer/TestInterval.cpp b/dom/media/gtest/mp4_demuxer/TestInterval.cpp new file mode 100644 index 0000000000..2572b1c392 --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/TestInterval.cpp @@ -0,0 +1,88 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "MP4Interval.h" + +using mozilla::MP4Interval; + +TEST(MP4Interval, Length) +{ + MP4Interval<int> i(15, 25); + EXPECT_EQ(10, i.Length()); +} + +TEST(MP4Interval, Intersection) +{ + MP4Interval<int> i0(10, 20); + MP4Interval<int> i1(15, 25); + MP4Interval<int> i = i0.Intersection(i1); + EXPECT_EQ(15, i.start); + EXPECT_EQ(20, i.end); +} + +TEST(MP4Interval, Equals) +{ + MP4Interval<int> i0(10, 20); + MP4Interval<int> i1(10, 20); + EXPECT_EQ(i0, i1); + + MP4Interval<int> i2(5, 20); + EXPECT_NE(i0, i2); + + MP4Interval<int> i3(10, 15); + EXPECT_NE(i0, i2); +} + +TEST(MP4Interval, IntersectionVector) +{ + nsTArray<MP4Interval<int>> i0; + i0.AppendElement(MP4Interval<int>(5, 10)); + i0.AppendElement(MP4Interval<int>(20, 25)); + i0.AppendElement(MP4Interval<int>(40, 60)); + + nsTArray<MP4Interval<int>> i1; + i1.AppendElement(MP4Interval<int>(7, 15)); + i1.AppendElement(MP4Interval<int>(16, 27)); + i1.AppendElement(MP4Interval<int>(45, 50)); + i1.AppendElement(MP4Interval<int>(53, 57)); + + nsTArray<MP4Interval<int>> i; + MP4Interval<int>::Intersection(i0, i1, &i); + + EXPECT_EQ(4u, i.Length()); + + EXPECT_EQ(7, i[0].start); + EXPECT_EQ(10, i[0].end); + + EXPECT_EQ(20, i[1].start); + EXPECT_EQ(25, i[1].end); + + EXPECT_EQ(45, i[2].start); + EXPECT_EQ(50, i[2].end); + + EXPECT_EQ(53, i[3].start); + EXPECT_EQ(57, i[3].end); +} + +TEST(MP4Interval, Normalize) +{ + nsTArray<MP4Interval<int>> i; + i.AppendElement(MP4Interval<int>(20, 30)); + i.AppendElement(MP4Interval<int>(1, 8)); + i.AppendElement(MP4Interval<int>(5, 10)); + i.AppendElement(MP4Interval<int>(2, 7)); + + nsTArray<MP4Interval<int>> o; + MP4Interval<int>::Normalize(i, &o); + + EXPECT_EQ(2u, o.Length()); + + EXPECT_EQ(1, o[0].start); + EXPECT_EQ(10, o[0].end); + + EXPECT_EQ(20, o[1].start); + EXPECT_EQ(30, o[1].end); +} diff --git a/dom/media/gtest/mp4_demuxer/TestMP4.cpp b/dom/media/gtest/mp4_demuxer/TestMP4.cpp new file mode 100644 index 0000000000..e1e409c693 --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/TestMP4.cpp @@ -0,0 +1,133 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "mp4parse.h" + +#include <stdint.h> +#include <stdio.h> +#include <string.h> +#include <algorithm> +#include <vector> + +static intptr_t error_reader(uint8_t* buffer, uintptr_t size, void* userdata) { + return -1; +} + +struct read_vector { + explicit read_vector(FILE* file, size_t length); + explicit read_vector(size_t length); + + size_t location; + std::vector<uint8_t> buffer; +}; + +read_vector::read_vector(FILE* file, size_t length) : location(0) { + buffer.resize(length); + size_t read = fread(buffer.data(), sizeof(decltype(buffer)::value_type), + buffer.size(), file); + buffer.resize(read); +} + +read_vector::read_vector(size_t length) : location(0) { + buffer.resize(length, 0); +} + +static intptr_t vector_reader(uint8_t* buffer, uintptr_t size, void* userdata) { + if (!buffer || !userdata) { + return -1; + } + + auto source = reinterpret_cast<read_vector*>(userdata); + if (source->location > source->buffer.size()) { + return -1; + } + uintptr_t available = + source->buffer.data() ? source->buffer.size() - source->location : 0; + uintptr_t length = std::min(available, size); + if (length) { + memcpy(buffer, source->buffer.data() + source->location, length); + source->location += length; + } + return length; +} + +TEST(rust, MP4MetadataEmpty) +{ + Mp4parseStatus rv; + Mp4parseIo io; + Mp4parseParser* parser = nullptr; + + // Shouldn't be able to read with no context. + rv = mp4parse_new(nullptr, nullptr); + EXPECT_EQ(rv, MP4PARSE_STATUS_BAD_ARG); + + // Shouldn't be able to wrap an Mp4parseIo with null members. + io = {nullptr, nullptr}; + rv = mp4parse_new(&io, &parser); + EXPECT_EQ(rv, MP4PARSE_STATUS_BAD_ARG); + EXPECT_EQ(parser, nullptr); + + io = {nullptr, &io}; + rv = mp4parse_new(&io, &parser); + EXPECT_EQ(rv, MP4PARSE_STATUS_BAD_ARG); + EXPECT_EQ(parser, nullptr); + + // FIXME: this should probably be accepted. + io = {error_reader, nullptr}; + rv = mp4parse_new(&io, &parser); + EXPECT_EQ(rv, MP4PARSE_STATUS_BAD_ARG); + EXPECT_EQ(parser, nullptr); + + // Read method errors should propagate. + io = {error_reader, &io}; + rv = mp4parse_new(&io, &parser); + ASSERT_EQ(parser, nullptr); + EXPECT_EQ(rv, MP4PARSE_STATUS_IO); + + // Short buffers should fail. + read_vector buf(0); + io = {vector_reader, &buf}; + rv = mp4parse_new(&io, &parser); + ASSERT_EQ(parser, nullptr); + EXPECT_EQ(rv, MP4PARSE_STATUS_INVALID); + + buf.buffer.reserve(4097); + rv = mp4parse_new(&io, &parser); + ASSERT_EQ(parser, nullptr); + EXPECT_EQ(rv, MP4PARSE_STATUS_INVALID); + + // Empty buffers should fail. + buf.buffer.resize(4097, 0); + rv = mp4parse_new(&io, &parser); + ASSERT_EQ(parser, nullptr); + EXPECT_EQ(rv, MP4PARSE_STATUS_UNSUPPORTED); +} + +TEST(rust, MP4Metadata) +{ + FILE* f = fopen("street.mp4", "rb"); + ASSERT_TRUE(f != nullptr); + // Read just the moov header to work around the parser + // treating mid-box eof as an error. + // read_vector reader = read_vector(f, 1061); + struct stat s; + ASSERT_EQ(0, fstat(fileno(f), &s)); + read_vector reader = read_vector(f, s.st_size); + fclose(f); + + Mp4parseIo io = {vector_reader, &reader}; + Mp4parseParser* parser = nullptr; + Mp4parseStatus rv = mp4parse_new(&io, &parser); + ASSERT_NE(nullptr, parser); + EXPECT_EQ(MP4PARSE_STATUS_OK, rv); + + uint32_t tracks = 0; + rv = mp4parse_get_track_count(parser, &tracks); + EXPECT_EQ(MP4PARSE_STATUS_OK, rv); + EXPECT_EQ(2U, tracks); + + mp4parse_free(parser); +} diff --git a/dom/media/gtest/mp4_demuxer/TestParser.cpp b/dom/media/gtest/mp4_demuxer/TestParser.cpp new file mode 100644 index 0000000000..13f889908b --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/TestParser.cpp @@ -0,0 +1,991 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "js/Conversions.h" +#include "MediaData.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Preferences.h" +#include "mozilla/Tuple.h" +#include "BufferStream.h" +#include "MP4Metadata.h" +#include "MoofParser.h" +#include "TelemetryFixture.h" +#include "TelemetryTestHelpers.h" + +class TestStream; +namespace mozilla { +DDLoggedTypeNameAndBase(::TestStream, ByteStream); +} // namespace mozilla + +using namespace mozilla; + +static const uint32_t E = MP4Metadata::NumberTracksError(); + +class TestStream : public ByteStream, + public DecoderDoctorLifeLogger<TestStream> { + public: + TestStream(const uint8_t* aBuffer, size_t aSize) + : mHighestSuccessfulEndOffset(0), mBuffer(aBuffer), mSize(aSize) {} + bool ReadAt(int64_t aOffset, void* aData, size_t aLength, + size_t* aBytesRead) override { + if (aOffset < 0 || aOffset > static_cast<int64_t>(mSize)) { + return false; + } + // After the test, 0 <= aOffset <= mSize <= SIZE_MAX, so it's safe to cast + // to size_t. + size_t offset = static_cast<size_t>(aOffset); + // Don't read past the end (but it's not an error to try). + if (aLength > mSize - offset) { + aLength = mSize - offset; + } + // Now, 0 <= offset <= offset + aLength <= mSize <= SIZE_MAX. + *aBytesRead = aLength; + memcpy(aData, mBuffer + offset, aLength); + if (mHighestSuccessfulEndOffset < offset + aLength) { + mHighestSuccessfulEndOffset = offset + aLength; + } + return true; + } + bool CachedReadAt(int64_t aOffset, void* aData, size_t aLength, + size_t* aBytesRead) override { + return ReadAt(aOffset, aData, aLength, aBytesRead); + } + bool Length(int64_t* aLength) override { + *aLength = mSize; + return true; + } + void DiscardBefore(int64_t aOffset) override {} + + // Offset past the last character ever read. 0 when nothing read yet. + size_t mHighestSuccessfulEndOffset; + + protected: + virtual ~TestStream() = default; + + const uint8_t* mBuffer; + size_t mSize; +}; + +TEST(MP4Metadata, EmptyStream) +{ + RefPtr<ByteStream> stream = new TestStream(nullptr, 0); + + MP4Metadata::ResultAndByteBuffer metadataBuffer = + MP4Metadata::Metadata(stream); + EXPECT_TRUE(NS_OK != metadataBuffer.Result()); + EXPECT_FALSE(static_cast<bool>(metadataBuffer.Ref())); + + MP4Metadata metadata(stream); + EXPECT_TRUE(0u == + metadata.GetNumberTracks(TrackInfo::kUndefinedTrack).Ref() || + E == metadata.GetNumberTracks(TrackInfo::kUndefinedTrack).Ref()); + EXPECT_TRUE(0u == metadata.GetNumberTracks(TrackInfo::kAudioTrack).Ref() || + E == metadata.GetNumberTracks(TrackInfo::kAudioTrack).Ref()); + EXPECT_TRUE(0u == metadata.GetNumberTracks(TrackInfo::kVideoTrack).Ref() || + E == metadata.GetNumberTracks(TrackInfo::kVideoTrack).Ref()); + EXPECT_TRUE(0u == metadata.GetNumberTracks(TrackInfo::kTextTrack).Ref() || + E == metadata.GetNumberTracks(TrackInfo::kTextTrack).Ref()); + EXPECT_FALSE(metadata.GetTrackInfo(TrackInfo::kAudioTrack, 0).Ref()); + EXPECT_FALSE(metadata.GetTrackInfo(TrackInfo::kVideoTrack, 0).Ref()); + EXPECT_FALSE(metadata.GetTrackInfo(TrackInfo::kTextTrack, 0).Ref()); + // We can seek anywhere in any MPEG4. + EXPECT_TRUE(metadata.CanSeek()); + EXPECT_FALSE(metadata.Crypto().Ref()->valid); +} + +TEST(MoofParser, EmptyStream) +{ + RefPtr<ByteStream> stream = new TestStream(nullptr, 0); + + MoofParser parser(stream, AsVariant(ParseAllTracks{}), false); + EXPECT_EQ(0u, parser.mOffset); + EXPECT_TRUE(parser.ReachedEnd()); + + MediaByteRangeSet byteRanges; + EXPECT_FALSE(parser.RebuildFragmentedIndex(byteRanges)); + + EXPECT_TRUE(parser.GetCompositionRange(byteRanges).IsNull()); + EXPECT_TRUE(parser.mInitRange.IsEmpty()); + EXPECT_EQ(0u, parser.mOffset); + EXPECT_TRUE(parser.ReachedEnd()); + RefPtr<MediaByteBuffer> metadataBuffer = parser.Metadata(); + EXPECT_FALSE(metadataBuffer); + EXPECT_TRUE(parser.FirstCompleteMediaSegment().IsEmpty()); + EXPECT_TRUE(parser.FirstCompleteMediaHeader().IsEmpty()); +} + +nsTArray<uint8_t> ReadTestFile(const char* aFilename) { + if (!aFilename) { + return {}; + } + FILE* f = fopen(aFilename, "rb"); + if (!f) { + return {}; + } + + if (fseek(f, 0, SEEK_END) != 0) { + fclose(f); + return {}; + } + long position = ftell(f); + // I know EOF==-1, so this test is made obsolete by '<0', but I don't want + // the code to rely on that. + if (position == 0 || position == EOF || position < 0) { + fclose(f); + return {}; + } + if (fseek(f, 0, SEEK_SET) != 0) { + fclose(f); + return {}; + } + + size_t len = static_cast<size_t>(position); + nsTArray<uint8_t> buffer(len); + buffer.SetLength(len); + size_t read = fread(buffer.Elements(), 1, len, f); + fclose(f); + if (read != len) { + return {}; + } + + return buffer; +} + +struct TestFileData { + const char* mFilename; + bool mParseResult; + uint32_t mNumberVideoTracks; + bool mHasVideoIndice; + int64_t mVideoDuration; // For first video track, -1 if N/A. + int32_t mWidth; + int32_t mHeight; + uint32_t mNumberAudioTracks; + int64_t mAudioDuration; // For first audio track, -1 if N/A. + bool mHasCrypto; // Note, MP4Metadata only considers pssh box for crypto. + uint64_t mMoofReachedOffset; // or 0 for the end. + bool mValidMoof; + bool mHeader; + int8_t mAudioProfile; +}; + +static const TestFileData testFiles[] = { + // filename parses? #V hasVideoIndex vDur w h #A aDur hasCrypto? moofOffset + // validMoof? hasHeader? audio_profile + {"test_case_1156505.mp4", false, 0, false, -1, 0, 0, 0, -1, false, 152, + false, false, 0}, // invalid ''trak box + {"test_case_1181213.mp4", true, 1, true, 416666, 320, 240, 1, 477460, true, + 0, false, false, 2}, + {"test_case_1181215.mp4", true, 0, false, -1, 0, 0, 0, -1, false, 0, false, + false, 0}, + {"test_case_1181223.mp4", false, 0, false, 416666, 320, 240, 0, -1, false, + 0, false, false, 0}, + {"test_case_1181719.mp4", false, 0, false, -1, 0, 0, 0, -1, false, 0, false, + false, 0}, + {"test_case_1185230.mp4", true, 2, true, 416666, 320, 240, 2, 5, false, 0, + false, false, 2}, + {"test_case_1187067.mp4", true, 1, true, 80000, 160, 90, 0, -1, false, 0, + false, false, 0}, + {"test_case_1200326.mp4", false, 0, false, -1, 0, 0, 0, -1, false, 0, false, + false, 0}, + {"test_case_1204580.mp4", true, 1, true, 502500, 320, 180, 0, -1, false, 0, + false, false, 0}, + {"test_case_1216748.mp4", false, 0, false, -1, 0, 0, 0, -1, false, 152, + false, false, 0}, // invalid 'trak' box + {"test_case_1296473.mp4", false, 0, false, -1, 0, 0, 0, -1, false, 0, false, + false, 0}, + {"test_case_1296532.mp4", true, 1, true, 5589333, 560, 320, 1, 5589333, + true, 0, true, true, 2}, + {"test_case_1301065.mp4", true, 0, false, -1, 0, 0, 1, 100079991719000000, + false, 0, false, false, 2}, + {"test_case_1301065-u32max.mp4", true, 0, false, -1, 0, 0, 1, 97391548639, + false, 0, false, false, 2}, + {"test_case_1301065-max-ez.mp4", true, 0, false, -1, 0, 0, 1, + 209146758205306, false, 0, false, false, 2}, + {"test_case_1301065-harder.mp4", true, 0, false, -1, 0, 0, 1, + 209146758205328, false, 0, false, false, 2}, + {"test_case_1301065-max-ok.mp4", true, 0, false, -1, 0, 0, 1, + 9223372036854775804, false, 0, false, false, 2}, + // The duration is overflow for int64_t in TestFileData, parser uses + // uint64_t so + // this file is ignore. + //{ "test_case_1301065-overfl.mp4", 0, -1, 0, 0, 1, 9223372036854775827, + // false, 0, + // false, false, 2 + // }, + {"test_case_1301065-i64max.mp4", true, 0, false, -1, 0, 0, 0, -1, false, 0, + false, false, 0}, + {"test_case_1301065-i64min.mp4", true, 0, false, -1, 0, 0, 0, -1, false, 0, + false, false, 0}, + {"test_case_1301065-u64max.mp4", true, 0, false, -1, 0, 0, 1, 0, false, 0, + false, false, 2}, + {"test_case_1329061.mov", false, 0, false, -1, 0, 0, 1, 234567981, false, 0, + false, false, 2}, + {"test_case_1351094.mp4", true, 0, false, -1, 0, 0, 0, -1, false, 0, true, + true, 0}, + {"test_case_1389299.mp4", true, 1, true, 5589333, 560, 320, 1, 5589333, + true, 0, true, true, 2}, + + {"test_case_1389527.mp4", true, 1, false, 5005000, 80, 128, 1, 4992000, + false, 0, false, false, 2}, + {"test_case_1395244.mp4", true, 1, true, 416666, 320, 240, 1, 477460, false, + 0, false, false, 2}, + {"test_case_1388991.mp4", true, 0, false, -1, 0, 0, 1, 30000181, false, 0, + false, false, 2}, + {"test_case_1380468.mp4", false, 0, false, 0, 0, 0, 0, 0, false, 0, false, + false, 0}, + {"test_case_1410565.mp4", false, 0, false, 0, 0, 0, 0, 0, false, 955100, + true, true, 2}, // negative 'timescale' + {"test_case_1513651-2-sample-description-entries.mp4", true, 1, true, + 9843344, 400, 300, 0, -1, true, 0, false, false, 0}, + {"test_case_1519617-cenc-init-with-track_id-0.mp4", true, 1, true, 0, 1272, + 530, 0, -1, false, 0, false, false, + 0}, // Uses bad track id 0 and has a sinf but no pssh + {"test_case_1519617-track2-trafs-removed.mp4", true, 1, true, 10032000, 400, + 300, 1, 10032000, false, 0, true, true, 2}, + {"test_case_1519617-video-has-track_id-0.mp4", true, 1, true, 10032000, 400, + 300, 1, 10032000, false, 0, true, true, 2}, // Uses bad track id 0 +}; + +TEST(MP4Metadata, test_case_mp4) +{ + const TestFileData* tests = nullptr; + size_t length = 0; + + tests = testFiles; + length = ArrayLength(testFiles); + + for (size_t test = 0; test < length; ++test) { + nsTArray<uint8_t> buffer = ReadTestFile(tests[test].mFilename); + ASSERT_FALSE(buffer.IsEmpty()); + RefPtr<ByteStream> stream = + new TestStream(buffer.Elements(), buffer.Length()); + + MP4Metadata::ResultAndByteBuffer metadataBuffer = + MP4Metadata::Metadata(stream); + EXPECT_EQ(NS_OK, metadataBuffer.Result()); + EXPECT_TRUE(metadataBuffer.Ref()); + + MP4Metadata metadata(stream); + nsresult res = metadata.Parse(); + EXPECT_EQ(tests[test].mParseResult, NS_SUCCEEDED(res)) + << tests[test].mFilename; + if (!tests[test].mParseResult) { + continue; + } + + EXPECT_EQ(tests[test].mNumberAudioTracks, + metadata.GetNumberTracks(TrackInfo::kAudioTrack).Ref()) + << tests[test].mFilename; + EXPECT_EQ(tests[test].mNumberVideoTracks, + metadata.GetNumberTracks(TrackInfo::kVideoTrack).Ref()) + << tests[test].mFilename; + // If there is an error, we should expect an error code instead of zero + // for non-Audio/Video tracks. + const uint32_t None = (tests[test].mNumberVideoTracks == E) ? E : 0; + EXPECT_EQ(None, metadata.GetNumberTracks(TrackInfo::kUndefinedTrack).Ref()) + << tests[test].mFilename; + EXPECT_EQ(None, metadata.GetNumberTracks(TrackInfo::kTextTrack).Ref()) + << tests[test].mFilename; + EXPECT_FALSE(metadata.GetTrackInfo(TrackInfo::kUndefinedTrack, 0).Ref()); + MP4Metadata::ResultAndTrackInfo trackInfo = + metadata.GetTrackInfo(TrackInfo::kVideoTrack, 0); + if (!!tests[test].mNumberVideoTracks) { + ASSERT_TRUE(!!trackInfo.Ref()); + const VideoInfo* videoInfo = trackInfo.Ref()->GetAsVideoInfo(); + ASSERT_TRUE(!!videoInfo); + EXPECT_TRUE(videoInfo->IsValid()) << tests[test].mFilename; + EXPECT_TRUE(videoInfo->IsVideo()) << tests[test].mFilename; + EXPECT_EQ(tests[test].mVideoDuration, + videoInfo->mDuration.ToMicroseconds()) + << tests[test].mFilename; + EXPECT_EQ(tests[test].mWidth, videoInfo->mDisplay.width) + << tests[test].mFilename; + EXPECT_EQ(tests[test].mHeight, videoInfo->mDisplay.height) + << tests[test].mFilename; + + MP4Metadata::ResultAndIndice indices = + metadata.GetTrackIndice(videoInfo->mTrackId); + EXPECT_EQ(!!indices.Ref(), tests[test].mHasVideoIndice) + << tests[test].mFilename; + if (tests[test].mHasVideoIndice) { + for (size_t i = 0; i < indices.Ref()->Length(); i++) { + Index::Indice data; + EXPECT_TRUE(indices.Ref()->GetIndice(i, data)) + << tests[test].mFilename; + EXPECT_TRUE(data.start_offset <= data.end_offset) + << tests[test].mFilename; + EXPECT_TRUE(data.start_composition <= data.end_composition) + << tests[test].mFilename; + } + } + } + trackInfo = metadata.GetTrackInfo(TrackInfo::kAudioTrack, 0); + if (tests[test].mNumberAudioTracks == 0 || + tests[test].mNumberAudioTracks == E) { + EXPECT_TRUE(!trackInfo.Ref()) << tests[test].mFilename; + } else { + ASSERT_TRUE(!!trackInfo.Ref()); + const AudioInfo* audioInfo = trackInfo.Ref()->GetAsAudioInfo(); + ASSERT_TRUE(!!audioInfo); + EXPECT_TRUE(audioInfo->IsValid()) << tests[test].mFilename; + EXPECT_TRUE(audioInfo->IsAudio()) << tests[test].mFilename; + EXPECT_EQ(tests[test].mAudioDuration, + audioInfo->mDuration.ToMicroseconds()) + << tests[test].mFilename; + EXPECT_EQ(tests[test].mAudioProfile, audioInfo->mProfile) + << tests[test].mFilename; + if (tests[test].mAudioDuration != audioInfo->mDuration.ToMicroseconds()) { + MOZ_RELEASE_ASSERT(false); + } + + MP4Metadata::ResultAndIndice indices = + metadata.GetTrackIndice(audioInfo->mTrackId); + EXPECT_TRUE(!!indices.Ref()) << tests[test].mFilename; + for (size_t i = 0; i < indices.Ref()->Length(); i++) { + Index::Indice data; + EXPECT_TRUE(indices.Ref()->GetIndice(i, data)) << tests[test].mFilename; + EXPECT_TRUE(data.start_offset <= data.end_offset) + << tests[test].mFilename; + EXPECT_TRUE(int64_t(data.start_composition) <= + int64_t(data.end_composition)) + << tests[test].mFilename; + } + } + EXPECT_FALSE(metadata.GetTrackInfo(TrackInfo::kTextTrack, 0).Ref()) + << tests[test].mFilename; + // We can see anywhere in any MPEG4. + EXPECT_TRUE(metadata.CanSeek()) << tests[test].mFilename; + EXPECT_EQ(tests[test].mHasCrypto, metadata.Crypto().Ref()->valid) + << tests[test].mFilename; + } +} + +// This test was disabled by Bug 1224019 for producing way too much output. +// This test no longer produces such output, as we've moved away from +// stagefright, but it does take a long time to run. I can be useful to enable +// as a sanity check on changes to the parser, but is too taxing to run as part +// of normal test execution. +#if 0 +TEST(MP4Metadata, test_case_mp4_subsets) { + static const size_t step = 1u; + for (size_t test = 0; test < ArrayLength(testFiles); ++test) { + nsTArray<uint8_t> buffer = ReadTestFile(testFiles[test].mFilename); + ASSERT_FALSE(buffer.IsEmpty()); + ASSERT_LE(step, buffer.Length()); + // Just exercizing the parser starting at different points through the file, + // making sure it doesn't crash. + // No checks because results would differ for each position. + for (size_t offset = 0; offset < buffer.Length() - step; offset += step) { + size_t size = buffer.Length() - offset; + while (size > 0) { + RefPtr<TestStream> stream = + new TestStream(buffer.Elements() + offset, size); + + MP4Metadata::ResultAndByteBuffer metadataBuffer = + MP4Metadata::Metadata(stream); + MP4Metadata metadata(stream); + + if (stream->mHighestSuccessfulEndOffset <= 0) { + // No successful reads -> Cutting down the size won't change anything. + break; + } + if (stream->mHighestSuccessfulEndOffset < size) { + // Read up to a point before the end -> Resize down to that point. + size = stream->mHighestSuccessfulEndOffset; + } else { + // Read up to the end (or after?!) -> Just cut 1 byte. + size -= 1; + } + } + } + } +} +#endif + +#if !defined(XP_WIN) || !defined(MOZ_ASAN) // OOMs on Windows ASan +TEST(MoofParser, test_case_mp4) +{ + const TestFileData* tests = nullptr; + size_t length = 0; + + tests = testFiles; + length = ArrayLength(testFiles); + + for (size_t test = 0; test < length; ++test) { + nsTArray<uint8_t> buffer = ReadTestFile(tests[test].mFilename); + ASSERT_FALSE(buffer.IsEmpty()); + RefPtr<ByteStream> stream = + new TestStream(buffer.Elements(), buffer.Length()); + + MoofParser parser(stream, AsVariant(ParseAllTracks{}), false); + EXPECT_EQ(0u, parser.mOffset) << tests[test].mFilename; + EXPECT_FALSE(parser.ReachedEnd()) << tests[test].mFilename; + EXPECT_TRUE(parser.mInitRange.IsEmpty()) << tests[test].mFilename; + + RefPtr<MediaByteBuffer> metadataBuffer = parser.Metadata(); + EXPECT_TRUE(metadataBuffer) << tests[test].mFilename; + + EXPECT_FALSE(parser.mInitRange.IsEmpty()) << tests[test].mFilename; + const MediaByteRangeSet byteRanges( + MediaByteRange(0, int64_t(buffer.Length()))); + EXPECT_EQ(tests[test].mValidMoof, parser.RebuildFragmentedIndex(byteRanges)) + << tests[test].mFilename; + if (tests[test].mMoofReachedOffset == 0) { + EXPECT_EQ(buffer.Length(), parser.mOffset) << tests[test].mFilename; + EXPECT_TRUE(parser.ReachedEnd()) << tests[test].mFilename; + } else { + EXPECT_EQ(tests[test].mMoofReachedOffset, parser.mOffset) + << tests[test].mFilename; + EXPECT_FALSE(parser.ReachedEnd()) << tests[test].mFilename; + } + + EXPECT_FALSE(parser.mInitRange.IsEmpty()) << tests[test].mFilename; + EXPECT_TRUE(parser.GetCompositionRange(byteRanges).IsNull()) + << tests[test].mFilename; + EXPECT_TRUE(parser.FirstCompleteMediaSegment().IsEmpty()) + << tests[test].mFilename; + EXPECT_EQ(tests[test].mHeader, !parser.FirstCompleteMediaHeader().IsEmpty()) + << tests[test].mFilename; + } +} + +TEST(MoofParser, test_case_sample_description_entries) +{ + const TestFileData* tests = testFiles; + size_t length = ArrayLength(testFiles); + + for (size_t test = 0; test < length; ++test) { + nsTArray<uint8_t> buffer = ReadTestFile(tests[test].mFilename); + ASSERT_FALSE(buffer.IsEmpty()); + RefPtr<ByteStream> stream = + new TestStream(buffer.Elements(), buffer.Length()); + + // Parse the first track. Treating it as audio is hacky, but this doesn't + // affect how we read the sample description entries. + uint32_t trackNumber = 1; + MoofParser parser(stream, AsVariant(trackNumber), false); + EXPECT_EQ(0u, parser.mOffset) << tests[test].mFilename; + EXPECT_FALSE(parser.ReachedEnd()) << tests[test].mFilename; + EXPECT_TRUE(parser.mInitRange.IsEmpty()) << tests[test].mFilename; + + // Explicitly don't call parser.Metadata() so that the parser itself will + // read the metadata as if we're in a fragmented case. Otherwise the parser + // won't read the sample description table. + + const MediaByteRangeSet byteRanges( + MediaByteRange(0, int64_t(buffer.Length()))); + EXPECT_EQ(tests[test].mValidMoof, parser.RebuildFragmentedIndex(byteRanges)) + << tests[test].mFilename; + + // We only care about crypto data from the samples descriptions right now. + // This test should be expanded should we read further information. + if (tests[test].mHasCrypto) { + uint32_t numEncryptedEntries = 0; + // It's possible to have multiple sample description entries, but we only + // expect one to carry crypto info for encrypted tracks. + for (SampleDescriptionEntry entry : parser.mSampleDescriptions) { + if (entry.mIsEncryptedEntry) { + numEncryptedEntries++; + } + } + EXPECT_EQ(1u, numEncryptedEntries) << tests[test].mFilename; + } + } +} +#endif // !defined(XP_WIN) || !defined(MOZ_ASAN) + +// We should gracefully handle track_id 0 since Bug 1519617. We'd previously +// used id 0 to trigger special handling in the MoofParser to read multiple +// track metadata, but since muxers use track id 0 in the wild, we want to +// make sure they can't accidentally trigger such handling. +TEST(MoofParser, test_case_track_id_0_does_not_read_multitracks) +{ + const char* zeroTrackIdFileName = + "test_case_1519617-video-has-track_id-0.mp4"; + nsTArray<uint8_t> buffer = ReadTestFile(zeroTrackIdFileName); + + ASSERT_FALSE(buffer.IsEmpty()); + RefPtr<ByteStream> stream = + new TestStream(buffer.Elements(), buffer.Length()); + + // Parse track id 0. We expect to only get metadata from that track, not the + // other track with id 2. + const uint32_t videoTrackId = 0; + MoofParser parser(stream, AsVariant(videoTrackId), false); + + // Explicitly don't call parser.Metadata() so that the parser itself will + // read the metadata as if we're in a fragmented case. Otherwise we won't + // read the trak data. + + const MediaByteRangeSet byteRanges( + MediaByteRange(0, int64_t(buffer.Length()))); + EXPECT_TRUE(parser.RebuildFragmentedIndex(byteRanges)) + << "MoofParser should find a valid moof as the file contains one!"; + + // Verify we only have data from track 0, if we parsed multiple tracks we'd + // find some of the audio track metadata here. Only check for values that + // differ between tracks. + const uint32_t videoTimescale = 90000; + const uint32_t videoSampleDuration = 3000; + const uint32_t videoSampleFlags = 0x10000; + const uint32_t videoNumSampleDescriptionEntries = 1; + EXPECT_EQ(videoTimescale, parser.mMdhd.mTimescale) + << "Wrong timescale for video track! If value is 22050, we've read from " + "the audio track!"; + EXPECT_EQ(videoTrackId, parser.mTrex.mTrackId) + << "Wrong track id for video track! If value is 2, we've read from the " + "audio track!"; + EXPECT_EQ(videoSampleDuration, parser.mTrex.mDefaultSampleDuration) + << "Wrong sample duration for video track! If value is 1024, we've read " + "from the audio track!"; + EXPECT_EQ(videoSampleFlags, parser.mTrex.mDefaultSampleFlags) + << "Wrong sample flags for video track! If value is 0x2000000 (note " + "that's hex), we've read from the audio track!"; + EXPECT_EQ(videoNumSampleDescriptionEntries, + parser.mSampleDescriptions.Length()) + << "Wrong number of sample descriptions for video track! If value is 2, " + "then we've read sample description information from video and audio " + "tracks!"; +} + +// We should gracefully handle track_id 0 since Bug 1519617. This includes +// handling crypto data from the sinf box in the MoofParser. Note, as of the +// time of writing, MP4Metadata uses the presence of a pssh box to determine +// if its crypto member is valid. However, even on files where the pssh isn't +// in the init segment, the MoofParser should still read the sinf, as in this +// testcase. +TEST(MoofParser, test_case_track_id_0_reads_crypto_metadata) +{ + const char* zeroTrackIdFileName = + "test_case_1519617-cenc-init-with-track_id-0.mp4"; + nsTArray<uint8_t> buffer = ReadTestFile(zeroTrackIdFileName); + + ASSERT_FALSE(buffer.IsEmpty()); + RefPtr<ByteStream> stream = + new TestStream(buffer.Elements(), buffer.Length()); + + // Parse track id 0. We expect to only get metadata from that track, not the + // other track with id 2. + const uint32_t videoTrackId = 0; + MoofParser parser(stream, AsVariant(videoTrackId), false); + + // Explicitly don't call parser.Metadata() so that the parser itself will + // read the metadata as if we're in a fragmented case. Otherwise we won't + // read the trak data. + + const MediaByteRangeSet byteRanges( + MediaByteRange(0, int64_t(buffer.Length()))); + EXPECT_FALSE(parser.RebuildFragmentedIndex(byteRanges)) + << "MoofParser should not find a valid moof, this is just an init " + "segment!"; + + // Verify we only have data from track 0, if we parsed multiple tracks we'd + // find some of the audio track metadata here. Only check for values that + // differ between tracks. + const size_t numSampleDescriptionEntries = 1; + const uint32_t defaultPerSampleIVSize = 8; + const size_t keyIdLength = 16; + const uint32_t defaultKeyId[keyIdLength] = { + 0x43, 0xbe, 0x13, 0xd0, 0x26, 0xc9, 0x41, 0x54, + 0x8f, 0xed, 0xf9, 0x54, 0x1a, 0xef, 0x6b, 0x0e}; + EXPECT_TRUE(parser.mSinf.IsValid()) + << "Should have a sinf that has crypto data!"; + EXPECT_EQ(defaultPerSampleIVSize, parser.mSinf.mDefaultIVSize) + << "Wrong default per sample IV size for track! If 0 indicates we failed " + "to parse some crypto info!"; + for (size_t i = 0; i < keyIdLength; i++) { + EXPECT_EQ(defaultKeyId[i], parser.mSinf.mDefaultKeyID[i]) + << "Mismatched default key ID byte at index " << i + << " indicates we failed to parse some crypto info!"; + } + ASSERT_EQ(numSampleDescriptionEntries, parser.mSampleDescriptions.Length()) + << "Wrong number of sample descriptions for track! If 0, indicates we " + "failed to parse some expected crypto!"; + EXPECT_TRUE(parser.mSampleDescriptions[0].mIsEncryptedEntry) + << "Sample description should be marked as encrypted!"; +} + +// The MoofParser may be asked to parse metadata for multiple tracks, but then +// be presented with fragments/moofs that contain data for only a subset of +// those tracks. I.e. metadata contains information for tracks with ids 1 and 2, +// but then the moof parser only receives moofs with data for track id 1. We +// should parse such fragmented media. In this test the metadata contains info +// for track ids 1 and 2, but track 2's track fragment headers (traf) have been +// over written with free space boxes (free). +TEST(MoofParser, test_case_moofs_missing_trafs) +{ + const char* noTrafsForTrack2MoofsFileName = + "test_case_1519617-track2-trafs-removed.mp4"; + nsTArray<uint8_t> buffer = ReadTestFile(noTrafsForTrack2MoofsFileName); + + ASSERT_FALSE(buffer.IsEmpty()); + RefPtr<ByteStream> stream = + new TestStream(buffer.Elements(), buffer.Length()); + + // Create parser that will read metadata from all tracks. + MoofParser parser(stream, AsVariant(ParseAllTracks{}), false); + + // Explicitly don't call parser.Metadata() so that the parser itself will + // read the metadata as if we're in a fragmented case. Otherwise we won't + // read the trak data. + + const MediaByteRangeSet byteRanges( + MediaByteRange(0, int64_t(buffer.Length()))); + EXPECT_TRUE(parser.RebuildFragmentedIndex(byteRanges)) + << "MoofParser should find a valid moof, there's 2 in the file!"; + + // Verify we've found 2 moofs and that the parser was able to parse them. + const size_t numMoofs = 2; + EXPECT_EQ(numMoofs, parser.Moofs().Length()) + << "File has 2 moofs, we should have read both"; + for (size_t i = 0; i < parser.Moofs().Length(); i++) { + EXPECT_TRUE(parser.Moofs()[i].IsValid()) << "All moofs should be valid"; + } +} + +// This test was disabled by Bug 1224019 for producing way too much output. +// This test no longer produces such output, as we've moved away from +// stagefright, but it does take a long time to run. I can be useful to enable +// as a sanity check on changes to the parser, but is too taxing to run as part +// of normal test execution. +#if 0 +TEST(MoofParser, test_case_mp4_subsets) { + const size_t step = 1u; + for (size_t test = 0; test < ArrayLength(testFiles); ++test) { + nsTArray<uint8_t> buffer = ReadTestFile(testFiles[test].mFilename); + ASSERT_FALSE(buffer.IsEmpty()); + ASSERT_LE(step, buffer.Length()); + // Just exercizing the parser starting at different points through the file, + // making sure it doesn't crash. + // No checks because results would differ for each position. + for (size_t offset = 0; offset < buffer.Length() - step; offset += step) { + size_t size = buffer.Length() - offset; + while (size > 0) { + RefPtr<TestStream> stream = + new TestStream(buffer.Elements() + offset, size); + + MoofParser parser(stream, AsVariant(ParseAllTracks{}), false); + MediaByteRangeSet byteRanges; + EXPECT_FALSE(parser.RebuildFragmentedIndex(byteRanges)); + parser.GetCompositionRange(byteRanges); + RefPtr<MediaByteBuffer> metadataBuffer = parser.Metadata(); + parser.FirstCompleteMediaSegment(); + parser.FirstCompleteMediaHeader(); + + if (stream->mHighestSuccessfulEndOffset <= 0) { + // No successful reads -> Cutting down the size won't change anything. + break; + } + if (stream->mHighestSuccessfulEndOffset < size) { + // Read up to a point before the end -> Resize down to that point. + size = stream->mHighestSuccessfulEndOffset; + } else { + // Read up to the end (or after?!) -> Just cut 1 byte. + size -= 1; + } + } + } + } +} +#endif + +uint8_t media_gtest_video_init_mp4[] = { + 0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d, + 0x00, 0x00, 0x00, 0x01, 0x69, 0x73, 0x6f, 0x6d, 0x61, 0x76, 0x63, 0x31, + 0x00, 0x00, 0x02, 0xd1, 0x6d, 0x6f, 0x6f, 0x76, 0x00, 0x00, 0x00, 0x6c, + 0x6d, 0x76, 0x68, 0x64, 0x00, 0x00, 0x00, 0x00, 0xc8, 0x49, 0x73, 0xf8, + 0xc8, 0x4a, 0xc5, 0x7a, 0x00, 0x00, 0x02, 0x58, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x18, + 0x69, 0x6f, 0x64, 0x73, 0x00, 0x00, 0x00, 0x00, 0x10, 0x80, 0x80, 0x80, + 0x07, 0x00, 0x4f, 0xff, 0xff, 0x29, 0x15, 0xff, 0x00, 0x00, 0x02, 0x0d, + 0x74, 0x72, 0x61, 0x6b, 0x00, 0x00, 0x00, 0x5c, 0x74, 0x6b, 0x68, 0x64, + 0x00, 0x00, 0x00, 0x01, 0xc8, 0x49, 0x73, 0xf8, 0xc8, 0x49, 0x73, 0xf9, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, 0x02, 0x80, 0x00, 0x00, 0x01, 0x68, 0x00, 0x00, + 0x00, 0x00, 0x01, 0xa9, 0x6d, 0x64, 0x69, 0x61, 0x00, 0x00, 0x00, 0x20, + 0x6d, 0x64, 0x68, 0x64, 0x00, 0x00, 0x00, 0x00, 0xc8, 0x49, 0x73, 0xf8, + 0xc8, 0x49, 0x73, 0xf9, 0x00, 0x00, 0x75, 0x30, 0x00, 0x00, 0x00, 0x00, + 0x55, 0xc4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x68, 0x64, 0x6c, 0x72, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0x69, 0x64, 0x65, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x47, 0x50, 0x41, 0x43, 0x20, 0x49, 0x53, 0x4f, 0x20, 0x56, 0x69, 0x64, + 0x65, 0x6f, 0x20, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x49, 0x6d, 0x69, 0x6e, 0x66, 0x00, 0x00, 0x00, 0x14, + 0x76, 0x6d, 0x68, 0x64, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x64, 0x69, 0x6e, 0x66, + 0x00, 0x00, 0x00, 0x1c, 0x64, 0x72, 0x65, 0x66, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x75, 0x72, 0x6c, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x09, 0x73, 0x74, 0x62, 0x6c, + 0x00, 0x00, 0x00, 0xad, 0x73, 0x74, 0x73, 0x64, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x9d, 0x61, 0x76, 0x63, 0x31, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x80, 0x01, 0x68, 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x18, 0xff, 0xff, 0x00, 0x00, 0x00, 0x33, 0x61, 0x76, + 0x63, 0x43, 0x01, 0x64, 0x00, 0x1f, 0xff, 0xe1, 0x00, 0x1b, 0x67, 0x64, + 0x00, 0x1f, 0xac, 0x2c, 0xc5, 0x02, 0x80, 0xbf, 0xe5, 0xc0, 0x44, 0x00, + 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf2, 0x3c, 0x60, 0xc6, + 0x58, 0x01, 0x00, 0x05, 0x68, 0xe9, 0x2b, 0x2c, 0x8b, 0x00, 0x00, 0x00, + 0x14, 0x62, 0x74, 0x72, 0x74, 0x00, 0x01, 0x5a, 0xc2, 0x00, 0x24, 0x74, + 0x38, 0x00, 0x09, 0x22, 0x00, 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x74, + 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x63, 0x74, 0x74, 0x73, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x73, 0x63, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x73, 0x74, 0x73, + 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x63, 0x6f, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x6d, 0x76, 0x65, + 0x78, 0x00, 0x00, 0x00, 0x10, 0x6d, 0x65, 0x68, 0x64, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x05, 0x76, 0x18, 0x00, 0x00, 0x00, 0x20, 0x74, 0x72, 0x65, + 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x03, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00}; + +const uint32_t media_gtest_video_init_mp4_len = 745; + +TEST(MP4Metadata, EmptyCTTS) +{ + RefPtr<MediaByteBuffer> buffer = + new MediaByteBuffer(media_gtest_video_init_mp4_len); + buffer->AppendElements(media_gtest_video_init_mp4, + media_gtest_video_init_mp4_len); + RefPtr<BufferStream> stream = new BufferStream(buffer); + + MP4Metadata::ResultAndByteBuffer metadataBuffer = + MP4Metadata::Metadata(stream); + EXPECT_EQ(NS_OK, metadataBuffer.Result()); + EXPECT_TRUE(metadataBuffer.Ref()); + + MP4Metadata metadata(stream); + EXPECT_EQ(metadata.Parse(), NS_OK); + EXPECT_EQ(1u, metadata.GetNumberTracks(TrackInfo::kVideoTrack).Ref()); + MP4Metadata::ResultAndTrackInfo track = + metadata.GetTrackInfo(TrackInfo::kVideoTrack, 0); + EXPECT_TRUE(track.Ref() != nullptr); + // We can seek anywhere in any MPEG4. + EXPECT_TRUE(metadata.CanSeek()); + EXPECT_FALSE(metadata.Crypto().Ref()->valid); +} + +// Fixture so we test telemetry probes. +class MP4MetadataTelemetryFixture : public TelemetryTestFixture {}; + +TEST_F(MP4MetadataTelemetryFixture, Telemetry) { + // Helper to fetch the metadata from a file and send telemetry in the process. + auto UpdateMetadataAndHistograms = [](const char* testFileName) { + nsTArray<uint8_t> buffer = ReadTestFile(testFileName); + ASSERT_FALSE(buffer.IsEmpty()); + RefPtr<ByteStream> stream = + new TestStream(buffer.Elements(), buffer.Length()); + + MP4Metadata::ResultAndByteBuffer metadataBuffer = + MP4Metadata::Metadata(stream); + EXPECT_EQ(NS_OK, metadataBuffer.Result()); + EXPECT_TRUE(metadataBuffer.Ref()); + + MP4Metadata metadata(stream); + nsresult res = metadata.Parse(); + EXPECT_TRUE(NS_SUCCEEDED(res)); + auto audioTrackCount = metadata.GetNumberTracks(TrackInfo::kAudioTrack); + ASSERT_NE(audioTrackCount.Ref(), MP4Metadata::NumberTracksError()); + auto videoTrackCount = metadata.GetNumberTracks(TrackInfo::kVideoTrack); + ASSERT_NE(videoTrackCount.Ref(), MP4Metadata::NumberTracksError()); + + // Need to read the track data to get telemetry to fire. + for (uint32_t i = 0; i < audioTrackCount.Ref(); i++) { + metadata.GetTrackInfo(TrackInfo::kAudioTrack, i); + } + for (uint32_t i = 0; i < videoTrackCount.Ref(); i++) { + metadata.GetTrackInfo(TrackInfo::kVideoTrack, i); + } + }; + + AutoJSContextWithGlobal cx(mCleanGlobal); + + // Checks the current state of the histograms relating to sample description + // entries and verifies they're in an expected state. + // aExpectedMultipleCodecCounts is a tuple where the first value represents + // the number of expected 'false' count, and the second the expected 'true' + // count for the sample description entries have multiple codecs histogram. + // aExpectedMultipleCryptoCounts is the same, but for the sample description + // entires have multiple crypto histogram. + // aExpectedSampleDescriptionEntryCounts is a tuple with 6 values, each is + // the expected number of sample description seen. I.e, the first value in the + // tuple is the number of tracks we've seen with 0 sample descriptions, the + // second value with 1 sample description, and so on up to 5 sample + // descriptions. aFileName is the name of the most recent file we've parsed, + // and is used to log if our telem counts are not in an expected state. + auto CheckHistograms = + [this, &cx]( + const Tuple<uint32_t, uint32_t>& aExpectedMultipleCodecCounts, + const Tuple<uint32_t, uint32_t>& aExpectedMultipleCryptoCounts, + const Tuple<uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, + uint32_t>& aExpectedSampleDescriptionEntryCounts, + const char* aFileName) { + // Get a snapshot of the current histograms + JS::RootedValue snapshot(cx.GetJSContext()); + TelemetryTestHelpers::GetSnapshots(cx.GetJSContext(), mTelemetry, + "" /* this string is unused */, + &snapshot, false /* is_keyed */); + + // We'll use these to pull values out of the histograms. + JS::RootedValue values(cx.GetJSContext()); + JS::RootedValue value(cx.GetJSContext()); + + // Verify our multiple codecs count histogram. + JS::RootedValue multipleCodecsHistogram(cx.GetJSContext()); + TelemetryTestHelpers::GetProperty( + cx.GetJSContext(), + "MEDIA_MP4_PARSE_SAMPLE_DESCRIPTION_ENTRIES_HAVE_MULTIPLE_CODECS", + snapshot, &multipleCodecsHistogram); + ASSERT_TRUE(multipleCodecsHistogram.isObject()) + << "Multiple codecs histogram should exist!"; + + TelemetryTestHelpers::GetProperty(cx.GetJSContext(), "values", + multipleCodecsHistogram, &values); + // False count. + TelemetryTestHelpers::GetElement(cx.GetJSContext(), 0, values, &value); + uint32_t uValue = 0; + JS::ToUint32(cx.GetJSContext(), value, &uValue); + EXPECT_EQ(Get<0>(aExpectedMultipleCodecCounts), uValue) + << "Unexpected number of false multiple codecs after parsing " + << aFileName; + // True count. + TelemetryTestHelpers::GetElement(cx.GetJSContext(), 1, values, &value); + JS::ToUint32(cx.GetJSContext(), value, &uValue); + EXPECT_EQ(Get<1>(aExpectedMultipleCodecCounts), uValue) + << "Unexpected number of true multiple codecs after parsing " + << aFileName; + + // Verify our multiple crypto count histogram. + JS::RootedValue multipleCryptoHistogram(cx.GetJSContext()); + TelemetryTestHelpers::GetProperty( + cx.GetJSContext(), + "MEDIA_MP4_PARSE_SAMPLE_DESCRIPTION_ENTRIES_HAVE_MULTIPLE_CRYPTO", + snapshot, &multipleCryptoHistogram); + ASSERT_TRUE(multipleCryptoHistogram.isObject()) + << "Multiple crypto histogram should exist!"; + + TelemetryTestHelpers::GetProperty(cx.GetJSContext(), "values", + multipleCryptoHistogram, &values); + // False count. + TelemetryTestHelpers::GetElement(cx.GetJSContext(), 0, values, &value); + JS::ToUint32(cx.GetJSContext(), value, &uValue); + EXPECT_EQ(Get<0>(aExpectedMultipleCryptoCounts), uValue) + << "Unexpected number of false multiple cryptos after parsing " + << aFileName; + // True count. + TelemetryTestHelpers::GetElement(cx.GetJSContext(), 1, values, &value); + JS::ToUint32(cx.GetJSContext(), value, &uValue); + EXPECT_EQ(Get<1>(aExpectedMultipleCryptoCounts), uValue) + << "Unexpected number of true multiple cryptos after parsing " + << aFileName; + + // Verify our sample description entry count histogram. + JS::RootedValue numSamplesHistogram(cx.GetJSContext()); + TelemetryTestHelpers::GetProperty( + cx.GetJSContext(), "MEDIA_MP4_PARSE_NUM_SAMPLE_DESCRIPTION_ENTRIES", + snapshot, &numSamplesHistogram); + ASSERT_TRUE(numSamplesHistogram.isObject()) + << "Num sample description entries histogram should exist!"; + + TelemetryTestHelpers::GetProperty(cx.GetJSContext(), "values", + numSamplesHistogram, &values); + + TelemetryTestHelpers::GetElement(cx.GetJSContext(), 0, values, &value); + JS::ToUint32(cx.GetJSContext(), value, &uValue); + EXPECT_EQ(Get<0>(aExpectedSampleDescriptionEntryCounts), uValue) + << "Unexpected number of 0 sample entry descriptions after parsing " + << aFileName; + TelemetryTestHelpers::GetElement(cx.GetJSContext(), 1, values, &value); + JS::ToUint32(cx.GetJSContext(), value, &uValue); + EXPECT_EQ(Get<1>(aExpectedSampleDescriptionEntryCounts), uValue) + << "Unexpected number of 1 sample entry descriptions after parsing " + << aFileName; + TelemetryTestHelpers::GetElement(cx.GetJSContext(), 2, values, &value); + JS::ToUint32(cx.GetJSContext(), value, &uValue); + EXPECT_EQ(Get<2>(aExpectedSampleDescriptionEntryCounts), uValue) + << "Unexpected number of 2 sample entry descriptions after parsing " + << aFileName; + TelemetryTestHelpers::GetElement(cx.GetJSContext(), 3, values, &value); + JS::ToUint32(cx.GetJSContext(), value, &uValue); + EXPECT_EQ(Get<3>(aExpectedSampleDescriptionEntryCounts), uValue) + << "Unexpected number of 3 sample entry descriptions after parsing " + << aFileName; + TelemetryTestHelpers::GetElement(cx.GetJSContext(), 4, values, &value); + JS::ToUint32(cx.GetJSContext(), value, &uValue); + EXPECT_EQ(Get<4>(aExpectedSampleDescriptionEntryCounts), uValue) + << "Unexpected number of 4 sample entry descriptions after parsing " + << aFileName; + TelemetryTestHelpers::GetElement(cx.GetJSContext(), 5, values, &value); + JS::ToUint32(cx.GetJSContext(), value, &uValue); + EXPECT_EQ(Get<5>(aExpectedSampleDescriptionEntryCounts), uValue) + << "Unexpected number of 5 sample entry descriptions after parsing " + << aFileName; + }; + + // Clear histograms + TelemetryTestHelpers::GetAndClearHistogram( + cx.GetJSContext(), mTelemetry, + nsLiteralCString( + "MEDIA_MP4_PARSE_SAMPLE_DESCRIPTION_ENTRIES_HAVE_MULTIPLE_CODECS"), + false /* is_keyed */); + + TelemetryTestHelpers::GetAndClearHistogram( + cx.GetJSContext(), mTelemetry, + nsLiteralCString( + "MEDIA_MP4_PARSE_SAMPLE_DESCRIPTION_ENTRIES_HAVE_MULTIPLE_CRYPTO"), + false /* is_keyed */); + + TelemetryTestHelpers::GetAndClearHistogram( + cx.GetJSContext(), mTelemetry, + "MEDIA_MP4_PARSE_NUM_SAMPLE_DESCRIPTION_ENTRIES"_ns, + false /* is_keyed */); + + // The snapshot won't have any data in it until we populate our histograms, so + // we don't check for a baseline here. Just read out first MP4 metadata. + + // Grab one of the test cases we know should parse and parse it, this should + // trigger telemetry gathering. + + // This file contains 2 moovs, each with a video and audio track with one + // sample description entry. So we should see 4 tracks, each with a single + // codec, no crypto, and a single sample description entry. + UpdateMetadataAndHistograms("test_case_1185230.mp4"); + + // Verify our histograms are updated. + CheckHistograms( + MakeTuple<uint32_t, uint32_t>(4, 0), MakeTuple<uint32_t, uint32_t>(4, 0), + MakeTuple<uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t>( + 0, 4, 0, 0, 0, 0), + "test_case_1185230.mp4"); + + // Parse another test case. This one has a single moov with a single video + // track. However, the track has two sample description entries, and our + // updated telemetry should reflect that. + UpdateMetadataAndHistograms( + "test_case_1513651-2-sample-description-entries.mp4"); + + // Verify our histograms are updated. + CheckHistograms( + MakeTuple<uint32_t, uint32_t>(5, 0), MakeTuple<uint32_t, uint32_t>(5, 0), + MakeTuple<uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t>( + 0, 4, 1, 0, 0, 0), + "test_case_1513651-2-sample-description-entries.mp4"); +} diff --git a/dom/media/gtest/mp4_demuxer/moz.build b/dom/media/gtest/mp4_demuxer/moz.build new file mode 100644 index 0000000000..5fbe4a461b --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/moz.build @@ -0,0 +1,66 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Library("mp4_demuxer_gtest") + +if CONFIG["OS_TARGET"] != "Android": + SOURCES += [ + "TestParser.cpp", + ] + +SOURCES += [ + "TestInterval.cpp", +] + +TEST_HARNESS_FILES.gtest += [ + "test_case_1156505.mp4", + "test_case_1181213.mp4", + "test_case_1181215.mp4", + "test_case_1181223.mp4", + "test_case_1181719.mp4", + "test_case_1185230.mp4", + "test_case_1187067.mp4", + "test_case_1200326.mp4", + "test_case_1204580.mp4", + "test_case_1216748.mp4", + "test_case_1296473.mp4", + "test_case_1296532.mp4", + "test_case_1301065-harder.mp4", + "test_case_1301065-i64max.mp4", + "test_case_1301065-i64min.mp4", + "test_case_1301065-max-ez.mp4", + "test_case_1301065-max-ok.mp4", + "test_case_1301065-overfl.mp4", + "test_case_1301065-u32max.mp4", + "test_case_1301065-u64max.mp4", + "test_case_1301065.mp4", + "test_case_1329061.mov", + "test_case_1351094.mp4", + "test_case_1380468.mp4", + "test_case_1388991.mp4", + "test_case_1389299.mp4", + "test_case_1389527.mp4", + "test_case_1395244.mp4", + "test_case_1410565.mp4", + "test_case_1513651-2-sample-description-entries.mp4", + "test_case_1519617-cenc-init-with-track_id-0.mp4", + "test_case_1519617-track2-trafs-removed.mp4", + "test_case_1519617-video-has-track_id-0.mp4", +] + +UNIFIED_SOURCES += [ + "TestMP4.cpp", +] + +TEST_HARNESS_FILES.gtest += [ + "../../test/street.mp4", +] +LOCAL_INCLUDES += [ + "../../mp4", + "/toolkit/components/telemetry/tests/gtest", +] + +FINAL_LIBRARY = "xul-gtest" diff --git a/dom/media/gtest/mp4_demuxer/test_case_1156505.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1156505.mp4 Binary files differnew file mode 100644 index 0000000000..687b06ee1f --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1156505.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1181213.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1181213.mp4 Binary files differnew file mode 100644 index 0000000000..e2326edb4e --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1181213.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1181215.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1181215.mp4 Binary files differnew file mode 100644 index 0000000000..7adba3836f --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1181215.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1181223.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1181223.mp4 Binary files differnew file mode 100644 index 0000000000..2aa2d5abfd --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1181223.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1181719.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1181719.mp4 Binary files differnew file mode 100644 index 0000000000..6846edd6ed --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1181719.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1185230.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1185230.mp4 Binary files differnew file mode 100644 index 0000000000..ac5cbdbe85 --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1185230.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1187067.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1187067.mp4 Binary files differnew file mode 100644 index 0000000000..fdb396eeb3 --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1187067.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1200326.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1200326.mp4 Binary files differnew file mode 100644 index 0000000000..5b8b27d508 --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1200326.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1204580.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1204580.mp4 Binary files differnew file mode 100644 index 0000000000..4e55b05719 --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1204580.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1216748.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1216748.mp4 Binary files differnew file mode 100644 index 0000000000..7072f53bec --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1216748.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1296473.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1296473.mp4 Binary files differnew file mode 100644 index 0000000000..109eb51064 --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1296473.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1296532.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1296532.mp4 Binary files differnew file mode 100644 index 0000000000..5a5669bb89 --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1296532.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-harder.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-harder.mp4 Binary files differnew file mode 100644 index 0000000000..7d678b7c66 --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-harder.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-i64max.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-i64max.mp4 Binary files differnew file mode 100644 index 0000000000..5a3572f88c --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-i64max.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-i64min.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-i64min.mp4 Binary files differnew file mode 100644 index 0000000000..4d3eb366e1 --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-i64min.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-max-ez.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-max-ez.mp4 Binary files differnew file mode 100644 index 0000000000..17fbf411ed --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-max-ez.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-max-ok.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-max-ok.mp4 Binary files differnew file mode 100644 index 0000000000..a5e1e4610d --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-max-ok.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-overfl.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-overfl.mp4 Binary files differnew file mode 100644 index 0000000000..1ef24e932b --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-overfl.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-u32max.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-u32max.mp4 Binary files differnew file mode 100644 index 0000000000..b1d8b6ce7e --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-u32max.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-u64max.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-u64max.mp4 Binary files differnew file mode 100644 index 0000000000..419dcba2c1 --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-u64max.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065.mp4 Binary files differnew file mode 100644 index 0000000000..543a4fba3e --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1301065.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1329061.mov b/dom/media/gtest/mp4_demuxer/test_case_1329061.mov Binary files differnew file mode 100644 index 0000000000..4246b8f716 --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1329061.mov diff --git a/dom/media/gtest/mp4_demuxer/test_case_1351094.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1351094.mp4 Binary files differnew file mode 100644 index 0000000000..2dfd4c35ce --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1351094.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1380468.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1380468.mp4 Binary files differnew file mode 100644 index 0000000000..277252f313 --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1380468.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1388991.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1388991.mp4 Binary files differnew file mode 100644 index 0000000000..deb7aae33a --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1388991.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1389299.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1389299.mp4 Binary files differnew file mode 100644 index 0000000000..78dc390a3d --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1389299.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1389527.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1389527.mp4 Binary files differnew file mode 100644 index 0000000000..6406fcb8f8 --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1389527.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1395244.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1395244.mp4 Binary files differnew file mode 100644 index 0000000000..da43d017ed --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1395244.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1410565.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1410565.mp4 Binary files differnew file mode 100644 index 0000000000..ebeaa08354 --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1410565.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1513651-2-sample-description-entries.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1513651-2-sample-description-entries.mp4 Binary files differnew file mode 100644 index 0000000000..2f8f235a9b --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1513651-2-sample-description-entries.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1519617-cenc-init-with-track_id-0.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1519617-cenc-init-with-track_id-0.mp4 Binary files differnew file mode 100644 index 0000000000..e76e9f0894 --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1519617-cenc-init-with-track_id-0.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1519617-track2-trafs-removed.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1519617-track2-trafs-removed.mp4 Binary files differnew file mode 100644 index 0000000000..55bd57c7db --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1519617-track2-trafs-removed.mp4 diff --git a/dom/media/gtest/mp4_demuxer/test_case_1519617-video-has-track_id-0.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1519617-video-has-track_id-0.mp4 Binary files differnew file mode 100644 index 0000000000..8cb4dcc212 --- /dev/null +++ b/dom/media/gtest/mp4_demuxer/test_case_1519617-video-has-track_id-0.mp4 diff --git a/dom/media/gtest/negative_duration.mp4 b/dom/media/gtest/negative_duration.mp4 Binary files differnew file mode 100644 index 0000000000..de86bf497c --- /dev/null +++ b/dom/media/gtest/negative_duration.mp4 diff --git a/dom/media/gtest/noise.mp3 b/dom/media/gtest/noise.mp3 Binary files differnew file mode 100644 index 0000000000..e76b503502 --- /dev/null +++ b/dom/media/gtest/noise.mp3 diff --git a/dom/media/gtest/noise_vbr.mp3 b/dom/media/gtest/noise_vbr.mp3 Binary files differnew file mode 100644 index 0000000000..284ebe40bf --- /dev/null +++ b/dom/media/gtest/noise_vbr.mp3 diff --git a/dom/media/gtest/short-zero-in-moov.mp4 b/dom/media/gtest/short-zero-in-moov.mp4 Binary files differnew file mode 100644 index 0000000000..577318c8fa --- /dev/null +++ b/dom/media/gtest/short-zero-in-moov.mp4 diff --git a/dom/media/gtest/short-zero-inband.mov b/dom/media/gtest/short-zero-inband.mov Binary files differnew file mode 100644 index 0000000000..9c18642865 --- /dev/null +++ b/dom/media/gtest/short-zero-inband.mov diff --git a/dom/media/gtest/small-shot-false-positive.mp3 b/dom/media/gtest/small-shot-false-positive.mp3 Binary files differnew file mode 100644 index 0000000000..2f1e794051 --- /dev/null +++ b/dom/media/gtest/small-shot-false-positive.mp3 diff --git a/dom/media/gtest/small-shot-partial-xing.mp3 b/dom/media/gtest/small-shot-partial-xing.mp3 Binary files differnew file mode 100644 index 0000000000..99d68e3cbe --- /dev/null +++ b/dom/media/gtest/small-shot-partial-xing.mp3 diff --git a/dom/media/gtest/small-shot.mp3 b/dom/media/gtest/small-shot.mp3 Binary files differnew file mode 100644 index 0000000000..f9397a5106 --- /dev/null +++ b/dom/media/gtest/small-shot.mp3 diff --git a/dom/media/gtest/test.webm b/dom/media/gtest/test.webm Binary files differnew file mode 100644 index 0000000000..487914c4a3 --- /dev/null +++ b/dom/media/gtest/test.webm diff --git a/dom/media/gtest/test_case_1224361.vp8.ivf b/dom/media/gtest/test_case_1224361.vp8.ivf Binary files differnew file mode 100644 index 0000000000..e2fe942f0e --- /dev/null +++ b/dom/media/gtest/test_case_1224361.vp8.ivf diff --git a/dom/media/gtest/test_case_1224363.vp8.ivf b/dom/media/gtest/test_case_1224363.vp8.ivf Binary files differnew file mode 100644 index 0000000000..6d2e4e0206 --- /dev/null +++ b/dom/media/gtest/test_case_1224363.vp8.ivf diff --git a/dom/media/gtest/test_case_1224369.vp8.ivf b/dom/media/gtest/test_case_1224369.vp8.ivf Binary files differnew file mode 100644 index 0000000000..2f8deb1148 --- /dev/null +++ b/dom/media/gtest/test_case_1224369.vp8.ivf diff --git a/dom/media/gtest/test_vbri.mp3 b/dom/media/gtest/test_vbri.mp3 Binary files differnew file mode 100644 index 0000000000..efd7450338 --- /dev/null +++ b/dom/media/gtest/test_vbri.mp3 |