summaryrefslogtreecommitdiffstats
path: root/dom/media/gtest
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /dom/media/gtest
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/media/gtest')
-rw-r--r--dom/media/gtest/AudioGenerator.h80
-rw-r--r--dom/media/gtest/AudioVerifier.h135
-rw-r--r--dom/media/gtest/Cargo.toml7
-rw-r--r--dom/media/gtest/GMPTestMonitor.h41
-rw-r--r--dom/media/gtest/MockCubeb.cpp447
-rw-r--r--dom/media/gtest/MockCubeb.h586
-rw-r--r--dom/media/gtest/MockMediaResource.cpp91
-rw-r--r--dom/media/gtest/MockMediaResource.h56
-rw-r--r--dom/media/gtest/TestAudioBuffers.cpp59
-rw-r--r--dom/media/gtest/TestAudioCallbackDriver.cpp224
-rw-r--r--dom/media/gtest/TestAudioCompactor.cpp131
-rw-r--r--dom/media/gtest/TestAudioDeviceEnumerator.cpp256
-rw-r--r--dom/media/gtest/TestAudioDriftCorrection.cpp404
-rw-r--r--dom/media/gtest/TestAudioMixer.cpp174
-rw-r--r--dom/media/gtest/TestAudioPacketizer.cpp163
-rw-r--r--dom/media/gtest/TestAudioRingBuffer.cpp993
-rw-r--r--dom/media/gtest/TestAudioSegment.cpp336
-rw-r--r--dom/media/gtest/TestAudioTrackEncoder.cpp282
-rw-r--r--dom/media/gtest/TestAudioTrackGraph.cpp774
-rw-r--r--dom/media/gtest/TestBenchmarkStorage.cpp92
-rw-r--r--dom/media/gtest/TestBitWriter.cpp72
-rw-r--r--dom/media/gtest/TestBlankVideoDataCreator.cpp30
-rw-r--r--dom/media/gtest/TestBufferReader.cpp38
-rw-r--r--dom/media/gtest/TestCDMStorage.cpp1074
-rw-r--r--dom/media/gtest/TestDataMutex.cpp46
-rw-r--r--dom/media/gtest/TestDecoderBenchmark.cpp66
-rw-r--r--dom/media/gtest/TestDriftCompensation.cpp84
-rw-r--r--dom/media/gtest/TestDynamicResampler.cpp1469
-rw-r--r--dom/media/gtest/TestGMPCrossOrigin.cpp208
-rw-r--r--dom/media/gtest/TestGMPRemoveAndDelete.cpp469
-rw-r--r--dom/media/gtest/TestGMPUtils.cpp84
-rw-r--r--dom/media/gtest/TestGroupId.cpp322
-rw-r--r--dom/media/gtest/TestIntervalSet.cpp819
-rw-r--r--dom/media/gtest/TestKeyValueStorage.cpp109
-rw-r--r--dom/media/gtest/TestMP3Demuxer.cpp559
-rw-r--r--dom/media/gtest/TestMP4Demuxer.cpp613
-rw-r--r--dom/media/gtest/TestMediaDataDecoder.cpp77
-rw-r--r--dom/media/gtest/TestMediaDataEncoder.cpp362
-rw-r--r--dom/media/gtest/TestMediaEventSource.cpp398
-rw-r--r--dom/media/gtest/TestMediaMIMETypes.cpp284
-rw-r--r--dom/media/gtest/TestMediaSpan.cpp110
-rw-r--r--dom/media/gtest/TestMuxer.cpp224
-rw-r--r--dom/media/gtest/TestOpusParser.cpp24
-rw-r--r--dom/media/gtest/TestRust.cpp10
-rw-r--r--dom/media/gtest/TestTimeUnit.cpp71
-rw-r--r--dom/media/gtest/TestVPXDecoding.cpp96
-rw-r--r--dom/media/gtest/TestVideoFrameConverter.cpp302
-rw-r--r--dom/media/gtest/TestVideoSegment.cpp44
-rw-r--r--dom/media/gtest/TestVideoTrackEncoder.cpp1569
-rw-r--r--dom/media/gtest/TestVideoUtils.cpp128
-rw-r--r--dom/media/gtest/TestWebMBuffered.cpp118
-rw-r--r--dom/media/gtest/TestWebMWriter.cpp358
-rw-r--r--dom/media/gtest/WaitFor.h89
-rw-r--r--dom/media/gtest/YUVBufferGenerator.cpp157
-rw-r--r--dom/media/gtest/YUVBufferGenerator.h32
-rw-r--r--dom/media/gtest/dash_dashinit.mp4bin0 -> 80388 bytes
-rw-r--r--dom/media/gtest/hello.rs6
-rw-r--r--dom/media/gtest/id3v2header.mp3bin0 -> 191302 bytes
-rw-r--r--dom/media/gtest/moz.build129
-rw-r--r--dom/media/gtest/mp4_demuxer/TestInterval.cpp88
-rw-r--r--dom/media/gtest/mp4_demuxer/TestMP4.cpp133
-rw-r--r--dom/media/gtest/mp4_demuxer/TestParser.cpp991
-rw-r--r--dom/media/gtest/mp4_demuxer/moz.build66
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1156505.mp4bin0 -> 296 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1181213.mp4bin0 -> 2834 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1181215.mp4bin0 -> 3086 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1181223.mp4bin0 -> 2834 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1181719.mp4bin0 -> 3095 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1185230.mp4bin0 -> 3250 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1187067.mp4bin0 -> 2835 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1200326.mp4bin0 -> 1694 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1204580.mp4bin0 -> 5833 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1216748.mp4bin0 -> 296 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1296473.mp4bin0 -> 5995 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1296532.mp4bin0 -> 152132 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1301065-harder.mp4bin0 -> 632 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1301065-i64max.mp4bin0 -> 632 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1301065-i64min.mp4bin0 -> 632 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1301065-max-ez.mp4bin0 -> 632 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1301065-max-ok.mp4bin0 -> 632 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1301065-overfl.mp4bin0 -> 632 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1301065-u32max.mp4bin0 -> 632 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1301065-u64max.mp4bin0 -> 632 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1301065.mp4bin0 -> 632 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1329061.movbin0 -> 93681 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1351094.mp4bin0 -> 80388 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1380468.mp4bin0 -> 2429 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1388991.mp4bin0 -> 288821 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1389299.mp4bin0 -> 152132 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1389527.mp4bin0 -> 92225 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1395244.mp4bin0 -> 13651 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1410565.mp4bin0 -> 955656 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1513651-2-sample-description-entries.mp4bin0 -> 1100 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1519617-cenc-init-with-track_id-0.mp4bin0 -> 767 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1519617-track2-trafs-removed.mp4bin0 -> 282228 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1519617-video-has-track_id-0.mp4bin0 -> 282024 bytes
-rw-r--r--dom/media/gtest/negative_duration.mp4bin0 -> 684 bytes
-rw-r--r--dom/media/gtest/noise.mp3bin0 -> 965257 bytes
-rw-r--r--dom/media/gtest/noise_vbr.mp3bin0 -> 583679 bytes
-rw-r--r--dom/media/gtest/short-zero-in-moov.mp4bin0 -> 13655 bytes
-rw-r--r--dom/media/gtest/short-zero-inband.movbin0 -> 93641 bytes
-rw-r--r--dom/media/gtest/small-shot-false-positive.mp3bin0 -> 6845 bytes
-rw-r--r--dom/media/gtest/small-shot-partial-xing.mp3bin0 -> 6825 bytes
-rw-r--r--dom/media/gtest/small-shot.mp3bin0 -> 6825 bytes
-rw-r--r--dom/media/gtest/test.webmbin0 -> 2015 bytes
-rw-r--r--dom/media/gtest/test_case_1224361.vp8.ivfbin0 -> 1497 bytes
-rw-r--r--dom/media/gtest/test_case_1224363.vp8.ivfbin0 -> 1388 bytes
-rw-r--r--dom/media/gtest/test_case_1224369.vp8.ivfbin0 -> 204 bytes
-rw-r--r--dom/media/gtest/test_vbri.mp3bin0 -> 16519 bytes
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, &params);
+ 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, &params);
+ 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
new file mode 100644
index 0000000000..d19068f36d
--- /dev/null
+++ b/dom/media/gtest/dash_dashinit.mp4
Binary files differ
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
new file mode 100644
index 0000000000..2f5585d02e
--- /dev/null
+++ b/dom/media/gtest/id3v2header.mp3
Binary files differ
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
new file mode 100644
index 0000000000..687b06ee1f
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1156505.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1181213.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1181213.mp4
new file mode 100644
index 0000000000..e2326edb4e
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1181213.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1181215.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1181215.mp4
new file mode 100644
index 0000000000..7adba3836f
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1181215.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1181223.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1181223.mp4
new file mode 100644
index 0000000000..2aa2d5abfd
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1181223.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1181719.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1181719.mp4
new file mode 100644
index 0000000000..6846edd6ed
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1181719.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1185230.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1185230.mp4
new file mode 100644
index 0000000000..ac5cbdbe85
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1185230.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1187067.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1187067.mp4
new file mode 100644
index 0000000000..fdb396eeb3
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1187067.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1200326.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1200326.mp4
new file mode 100644
index 0000000000..5b8b27d508
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1200326.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1204580.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1204580.mp4
new file mode 100644
index 0000000000..4e55b05719
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1204580.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1216748.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1216748.mp4
new file mode 100644
index 0000000000..7072f53bec
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1216748.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1296473.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1296473.mp4
new file mode 100644
index 0000000000..109eb51064
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1296473.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1296532.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1296532.mp4
new file mode 100644
index 0000000000..5a5669bb89
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1296532.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-harder.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-harder.mp4
new file mode 100644
index 0000000000..7d678b7c66
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-harder.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-i64max.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-i64max.mp4
new file mode 100644
index 0000000000..5a3572f88c
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-i64max.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-i64min.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-i64min.mp4
new file mode 100644
index 0000000000..4d3eb366e1
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-i64min.mp4
Binary files differ
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
new file mode 100644
index 0000000000..17fbf411ed
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-max-ez.mp4
Binary files differ
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
new file mode 100644
index 0000000000..a5e1e4610d
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-max-ok.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-overfl.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-overfl.mp4
new file mode 100644
index 0000000000..1ef24e932b
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-overfl.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-u32max.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-u32max.mp4
new file mode 100644
index 0000000000..b1d8b6ce7e
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-u32max.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-u64max.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-u64max.mp4
new file mode 100644
index 0000000000..419dcba2c1
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-u64max.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065.mp4
new file mode 100644
index 0000000000..543a4fba3e
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1301065.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1329061.mov b/dom/media/gtest/mp4_demuxer/test_case_1329061.mov
new file mode 100644
index 0000000000..4246b8f716
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1329061.mov
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1351094.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1351094.mp4
new file mode 100644
index 0000000000..2dfd4c35ce
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1351094.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1380468.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1380468.mp4
new file mode 100644
index 0000000000..277252f313
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1380468.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1388991.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1388991.mp4
new file mode 100644
index 0000000000..deb7aae33a
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1388991.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1389299.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1389299.mp4
new file mode 100644
index 0000000000..78dc390a3d
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1389299.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1389527.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1389527.mp4
new file mode 100644
index 0000000000..6406fcb8f8
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1389527.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1395244.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1395244.mp4
new file mode 100644
index 0000000000..da43d017ed
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1395244.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1410565.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1410565.mp4
new file mode 100644
index 0000000000..ebeaa08354
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1410565.mp4
Binary files differ
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
new file mode 100644
index 0000000000..2f8f235a9b
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1513651-2-sample-description-entries.mp4
Binary files differ
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
new file mode 100644
index 0000000000..e76e9f0894
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1519617-cenc-init-with-track_id-0.mp4
Binary files differ
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
new file mode 100644
index 0000000000..55bd57c7db
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1519617-track2-trafs-removed.mp4
Binary files differ
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
new file mode 100644
index 0000000000..8cb4dcc212
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1519617-video-has-track_id-0.mp4
Binary files differ
diff --git a/dom/media/gtest/negative_duration.mp4 b/dom/media/gtest/negative_duration.mp4
new file mode 100644
index 0000000000..de86bf497c
--- /dev/null
+++ b/dom/media/gtest/negative_duration.mp4
Binary files differ
diff --git a/dom/media/gtest/noise.mp3 b/dom/media/gtest/noise.mp3
new file mode 100644
index 0000000000..e76b503502
--- /dev/null
+++ b/dom/media/gtest/noise.mp3
Binary files differ
diff --git a/dom/media/gtest/noise_vbr.mp3 b/dom/media/gtest/noise_vbr.mp3
new file mode 100644
index 0000000000..284ebe40bf
--- /dev/null
+++ b/dom/media/gtest/noise_vbr.mp3
Binary files differ
diff --git a/dom/media/gtest/short-zero-in-moov.mp4 b/dom/media/gtest/short-zero-in-moov.mp4
new file mode 100644
index 0000000000..577318c8fa
--- /dev/null
+++ b/dom/media/gtest/short-zero-in-moov.mp4
Binary files differ
diff --git a/dom/media/gtest/short-zero-inband.mov b/dom/media/gtest/short-zero-inband.mov
new file mode 100644
index 0000000000..9c18642865
--- /dev/null
+++ b/dom/media/gtest/short-zero-inband.mov
Binary files differ
diff --git a/dom/media/gtest/small-shot-false-positive.mp3 b/dom/media/gtest/small-shot-false-positive.mp3
new file mode 100644
index 0000000000..2f1e794051
--- /dev/null
+++ b/dom/media/gtest/small-shot-false-positive.mp3
Binary files differ
diff --git a/dom/media/gtest/small-shot-partial-xing.mp3 b/dom/media/gtest/small-shot-partial-xing.mp3
new file mode 100644
index 0000000000..99d68e3cbe
--- /dev/null
+++ b/dom/media/gtest/small-shot-partial-xing.mp3
Binary files differ
diff --git a/dom/media/gtest/small-shot.mp3 b/dom/media/gtest/small-shot.mp3
new file mode 100644
index 0000000000..f9397a5106
--- /dev/null
+++ b/dom/media/gtest/small-shot.mp3
Binary files differ
diff --git a/dom/media/gtest/test.webm b/dom/media/gtest/test.webm
new file mode 100644
index 0000000000..487914c4a3
--- /dev/null
+++ b/dom/media/gtest/test.webm
Binary files differ
diff --git a/dom/media/gtest/test_case_1224361.vp8.ivf b/dom/media/gtest/test_case_1224361.vp8.ivf
new file mode 100644
index 0000000000..e2fe942f0e
--- /dev/null
+++ b/dom/media/gtest/test_case_1224361.vp8.ivf
Binary files differ
diff --git a/dom/media/gtest/test_case_1224363.vp8.ivf b/dom/media/gtest/test_case_1224363.vp8.ivf
new file mode 100644
index 0000000000..6d2e4e0206
--- /dev/null
+++ b/dom/media/gtest/test_case_1224363.vp8.ivf
Binary files differ
diff --git a/dom/media/gtest/test_case_1224369.vp8.ivf b/dom/media/gtest/test_case_1224369.vp8.ivf
new file mode 100644
index 0000000000..2f8deb1148
--- /dev/null
+++ b/dom/media/gtest/test_case_1224369.vp8.ivf
Binary files differ
diff --git a/dom/media/gtest/test_vbri.mp3 b/dom/media/gtest/test_vbri.mp3
new file mode 100644
index 0000000000..efd7450338
--- /dev/null
+++ b/dom/media/gtest/test_vbri.mp3
Binary files differ