summaryrefslogtreecommitdiff
path: root/dom/media/gtest
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/gtest')
-rw-r--r--dom/media/gtest/Cargo.toml7
-rw-r--r--dom/media/gtest/GMPTestMonitor.h47
-rw-r--r--dom/media/gtest/MockMediaDecoderOwner.h54
-rw-r--r--dom/media/gtest/MockMediaResource.cpp116
-rw-r--r--dom/media/gtest/MockMediaResource.h83
-rw-r--r--dom/media/gtest/TestAudioBuffers.cpp57
-rw-r--r--dom/media/gtest/TestAudioCompactor.cpp145
-rw-r--r--dom/media/gtest/TestAudioMixer.cpp165
-rw-r--r--dom/media/gtest/TestAudioPacketizer.cpp167
-rw-r--r--dom/media/gtest/TestAudioSegment.cpp257
-rw-r--r--dom/media/gtest/TestGMPCrossOrigin.cpp1552
-rw-r--r--dom/media/gtest/TestGMPRemoveAndDelete.cpp491
-rw-r--r--dom/media/gtest/TestGMPUtils.cpp61
-rw-r--r--dom/media/gtest/TestIntervalSet.cpp821
-rw-r--r--dom/media/gtest/TestMP3Demuxer.cpp454
-rw-r--r--dom/media/gtest/TestMP4Demuxer.cpp463
-rw-r--r--dom/media/gtest/TestMP4Reader.cpp217
-rw-r--r--dom/media/gtest/TestMediaDataDecoder.cpp72
-rw-r--r--dom/media/gtest/TestMediaEventSource.cpp337
-rw-r--r--dom/media/gtest/TestMediaFormatReader.cpp248
-rw-r--r--dom/media/gtest/TestMozPromise.cpp257
-rw-r--r--dom/media/gtest/TestRust.cpp9
-rw-r--r--dom/media/gtest/TestTimeUnit.cpp22
-rw-r--r--dom/media/gtest/TestTrackEncoder.cpp94
-rw-r--r--dom/media/gtest/TestVPXDecoding.cpp103
-rw-r--r--dom/media/gtest/TestVideoSegment.cpp51
-rw-r--r--dom/media/gtest/TestVideoTrackEncoder.cpp351
-rw-r--r--dom/media/gtest/TestVideoUtils.cpp80
-rw-r--r--dom/media/gtest/TestWebMBuffered.cpp119
-rw-r--r--dom/media/gtest/TestWebMWriter.cpp376
-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/mediasource_test.mp4bin0 -> 209832 bytes
-rw-r--r--dom/media/gtest/moz.build80
-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.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
46 files changed, 7362 insertions, 0 deletions
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..8ce6f8ddd8
--- /dev/null
+++ b/dom/media/gtest/GMPTestMonitor.h
@@ -0,0 +1,47 @@
+/* -*- 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 "nsThreadUtils.h"
+
+#ifndef __GMPTestMonitor_h__
+#define __GMPTestMonitor_h__
+
+class GMPTestMonitor
+{
+public:
+ GMPTestMonitor()
+ : mFinished(false)
+ {
+ }
+
+ void AwaitFinished()
+ {
+ MOZ_ASSERT(NS_IsMainThread());
+ while (!mFinished) {
+ NS_ProcessNextEvent(nullptr, true);
+ }
+ mFinished = false;
+ }
+
+private:
+ void MarkFinished()
+ {
+ MOZ_ASSERT(NS_IsMainThread());
+ mFinished = true;
+ }
+
+public:
+ void SetFinished()
+ {
+ NS_DispatchToMainThread(mozilla::NewNonOwningRunnableMethod(this,
+ &GMPTestMonitor::MarkFinished));
+ }
+
+private:
+ bool mFinished;
+};
+
+#endif // __GMPTestMonitor_h__
diff --git a/dom/media/gtest/MockMediaDecoderOwner.h b/dom/media/gtest/MockMediaDecoderOwner.h
new file mode 100644
index 0000000000..324f181416
--- /dev/null
+++ b/dom/media/gtest/MockMediaDecoderOwner.h
@@ -0,0 +1,54 @@
+/* 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_DECODER_OWNER_H_
+#define MOCK_MEDIA_DECODER_OWNER_H_
+
+#include "MediaDecoderOwner.h"
+#include "nsAutoPtr.h"
+
+namespace mozilla
+{
+
+class MockMediaDecoderOwner : public MediaDecoderOwner
+{
+public:
+ nsresult DispatchAsyncEvent(const nsAString& aName) override
+ {
+ return NS_OK;
+ }
+ void FireTimeUpdate(bool aPeriodic) override {}
+ bool GetPaused() override { return false; }
+ void MetadataLoaded(const MediaInfo* aInfo,
+ nsAutoPtr<const MetadataTags> aTags) override
+ {
+ }
+ void NetworkError() override {}
+ void DecodeError(const MediaResult& aError) override {}
+ bool HasError() const override { return false; }
+ void LoadAborted() override {}
+ void PlaybackEnded() override {}
+ void SeekStarted() override {}
+ void SeekCompleted() override {}
+ void DownloadProgressed() override {}
+ void UpdateReadyState() override {}
+ void FirstFrameLoaded() override {}
+ void DispatchEncrypted(const nsTArray<uint8_t>& aInitData,
+ const nsAString& aInitDataType) override {}
+ bool IsActive() const override { return true; }
+ bool IsHidden() const override { return false; }
+ void DownloadSuspended() override {}
+ void DownloadResumed(bool aForceNetworkLoading) override {}
+ void NotifySuspendedByCache(bool aIsSuspended) override {}
+ void NotifyDecoderPrincipalChanged() override {}
+ VideoFrameContainer* GetVideoFrameContainer() override
+ {
+ return nullptr;
+ }
+ void SetAudibleState(bool aAudible) override {}
+ void NotifyXPCOMShutdown() override {}
+};
+}
+
+#endif
diff --git a/dom/media/gtest/MockMediaResource.cpp b/dom/media/gtest/MockMediaResource.cpp
new file mode 100644
index 0000000000..4e75f8d9d3
--- /dev/null
+++ b/dom/media/gtest/MockMediaResource.cpp
@@ -0,0 +1,116 @@
+/* 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, const nsACString& aContentType)
+ : mFileHandle(nullptr)
+ , mFileName(aFileName)
+ , mContentType(aContentType)
+{
+}
+
+nsresult
+MockMediaResource::Open(nsIStreamListener** aStreamListener)
+{
+ 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);
+ size_t objectsRead = fread(aBuffer, aCount, 1, mFileHandle);
+ *aBytes = objectsRead == 1 ? aCount : 0;
+
+ 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 -1;
+}
+
+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..129ba1b727
--- /dev/null
+++ b/dom/media/gtest/MockMediaResource.h
@@ -0,0 +1,83 @@
+/* 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
+{
+
+class MockMediaResource : public MediaResource
+{
+public:
+ explicit MockMediaResource(const char* aFileName, const nsACString& aMimeType = NS_LITERAL_CSTRING("video/mp4"));
+ nsIURI* URI() const override { return nullptr; }
+ nsresult Close() override { return NS_OK; }
+ void Suspend(bool aCloseImmediately) override {}
+ void Resume() override {}
+ already_AddRefed<nsIPrincipal> GetCurrentPrincipal() override
+ {
+ return nullptr;
+ }
+ bool CanClone() override { return false; }
+ already_AddRefed<MediaResource> CloneData(MediaResourceCallback*)
+ override
+ {
+ return nullptr;
+ }
+ void SetReadMode(MediaCacheStream::ReadMode aMode) override {}
+ void SetPlaybackRate(uint32_t aBytesPerSecond) override {}
+ nsresult ReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount,
+ uint32_t* aBytes) override;
+ int64_t Tell() override { return 0; }
+ void Pin() override {}
+ void Unpin() override {}
+ double GetDownloadRate(bool* aIsReliable) override { return 0; }
+ 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;
+ }
+ bool IsSuspendedByCache() override { return false; }
+ bool IsSuspended() 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;
+ }
+
+ bool IsTransportSeekable() override { return true; }
+ nsresult Open(nsIStreamListener** aStreamListener) override;
+ nsresult GetCachedRanges(MediaByteRangeSet& aRanges) override;
+ const nsCString& GetContentType() const override
+ {
+ return mContentType;
+ }
+
+ void MockClearBufferedRanges();
+ void MockAddBufferedRange(int64_t aStart, int64_t aEnd);
+
+protected:
+ virtual ~MockMediaResource();
+
+private:
+ FILE* mFileHandle;
+ const char* mFileName;
+ MediaByteRangeSet mRanges;
+ Atomic<int> mEntry;
+ const nsCString mContentType;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/gtest/TestAudioBuffers.cpp b/dom/media/gtest/TestAudioBuffers.cpp
new file mode 100644
index 0000000000..b03886cec4
--- /dev/null
+++ b/dom/media/gtest/TestAudioBuffers.cpp
@@ -0,0 +1,57 @@
+/* -*- 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"
+
+const uint32_t FRAMES = 256;
+const uint32_t CHANNELS = 2;
+const uint32_t SAMPLES = CHANNELS * FRAMES;
+
+TEST(AudioBuffers, Test)
+{
+ mozilla::AudioCallbackBufferWrapper<float, CHANNELS> mBuffer;
+ mozilla::SpillBuffer<float, 128, CHANNELS> b;
+ float fromCallback[SAMPLES];
+ float other[SAMPLES];
+
+ for (uint32_t i = 0; i < SAMPLES; i++) {
+ other[i] = 1.0;
+ fromCallback[i] = 0.0;
+ }
+
+ // Set the buffer in the wrapper from the callback
+ mBuffer.SetBuffer(fromCallback, FRAMES);
+
+ // Fill the SpillBuffer with data.
+ ASSERT_TRUE(b.Fill(other, 15) == 15);
+ ASSERT_TRUE(b.Fill(other, 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 + 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, FRAMES) == 128);
+ ASSERT_TRUE(b.Fill(other, FRAMES) == 0);
+ ASSERT_TRUE(b.Empty(mBuffer) == 0);
+}
diff --git a/dom/media/gtest/TestAudioCompactor.cpp b/dom/media/gtest/TestAudioCompactor.cpp
new file mode 100644
index 0000000000..9a28254b35
--- /dev/null
+++ b/dom/media/gtest/TestAudioCompactor.cpp
@@ -0,0 +1,145 @@
+/* -*- 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 "MediaDecoderReader.h"
+
+using mozilla::AudioCompactor;
+using mozilla::AudioData;
+using mozilla::AudioDataValue;
+using mozilla::MediaDecoderReader;
+using mozilla::MediaQueue;
+
+class MemoryFunctor : public nsDequeFunctor {
+public:
+ MemoryFunctor() : mSize(0) {}
+ MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf);
+
+ void* operator()(void* aObject) override {
+ const AudioData* audioData = static_cast<const AudioData*>(aObject);
+ mSize += audioData->SizeOfIncludingThis(MallocSizeOf);
+ return nullptr;
+ }
+
+ 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/TestAudioMixer.cpp b/dom/media/gtest/TestAudioMixer.cpp
new file mode 100644
index 0000000000..4e075a5994
--- /dev/null
+++ b/dom/media/gtest/TestAudioMixer.cpp
@@ -0,0 +1,165 @@
+/* -*- 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(&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(&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(&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(&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..86615eb108
--- /dev/null
+++ b/dom/media/gtest/TestAudioPacketizer.cpp
@@ -0,0 +1,167 @@
+/* -*- 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 "../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(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(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++) {
+ int16_t* out = ap.Output();
+ Zero(out, 441);
+ delete[] out;
+ }
+ }
+ // 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);
+ int16_t* out = ap.Output();
+ IsSequence(out, 441 * channels, prevEnd);
+ delete[] out;
+ }
+ }
+ // 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);
+ int16_t* out = ap.Output();
+ int16_t* out2 = ap.Output();
+ IsSequence(out, 441 * channels, prevEnd0);
+ IsSequence(out2, 441 * channels, prevEnd1);
+ delete[] out;
+ delete[] out2;
+ }
+ }
+ // 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);
+ int16_t* out = ap.Output();
+ int16_t* out2 = ap.Output();
+ IsSequence(out, 441 * channels, prevEnd);
+ prevEnd += 441 * channels;
+ IsSequence(out2, 441 * channels, prevEnd);
+ prevEnd += 441 * channels;
+ delete[] out;
+ delete[] out2;
+ }
+ 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()) {
+ int16_t* packet = ap.Output();
+ for (uint32_t k = 0; k < ap.PacketSize(); k++) {
+ for (int32_t c = 0; c < channels; c++) {
+ ASSERT_TRUE(packet[k * channels + c] ==
+ static_cast<int16_t>(((2 << 14) * sine(outPhase))));
+ }
+ outPhase++;
+ }
+ delete [] packet;
+ }
+ }
+ }
+ }
+}
diff --git a/dom/media/gtest/TestAudioSegment.cpp b/dom/media/gtest/TestAudioSegment.cpp
new file mode 100644
index 0000000000..968b4b5a15
--- /dev/null
+++ b/dom/media/gtest/TestAudioSegment.cpp
@@ -0,0 +1,257 @@
+/* -*- 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>();
+}
+
+} // namespace audio_segment
diff --git a/dom/media/gtest/TestGMPCrossOrigin.cpp b/dom/media/gtest/TestGMPCrossOrigin.cpp
new file mode 100644
index 0000000000..036282153f
--- /dev/null
+++ b/dom/media/gtest/TestGMPCrossOrigin.cpp
@@ -0,0 +1,1552 @@
+/* -*- 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 "nsAutoPtr.h"
+#include "nsIObserverService.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPtr.h"
+#include "GMPTestMonitor.h"
+#include "GMPVideoDecoderProxy.h"
+#include "GMPVideoEncoderProxy.h"
+#include "GMPDecryptorProxy.h"
+#include "GMPServiceParent.h"
+#include "MediaPrefs.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsIFile.h"
+#include "nsISimpleEnumerator.h"
+#include "mozilla/Atomics.h"
+#include "nsNSSComponent.h"
+#include "mozilla/DebugOnly.h"
+#include "GMPDeviceBinding.h"
+#include "mozilla/dom/MediaKeyStatusMapBinding.h" // For MediaKeyStatus
+#include "mozilla/dom/MediaKeyMessageEventBinding.h" // For MediaKeyMessageType
+
+#if defined(XP_WIN)
+#include "mozilla/WindowsVersion.h"
+#endif
+
+using namespace std;
+
+using namespace mozilla;
+using namespace mozilla::gmp;
+
+struct GMPTestRunner
+{
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPTestRunner)
+
+ GMPTestRunner() { MediaPrefs::GetSingleton(); }
+ 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() { }
+};
+
+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, 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(NS_LITERAL_CSTRING("h264"));
+ tags.AppendElement(NS_LITERAL_CSTRING("fake"));
+
+ RefPtr<GeckoMediaPluginService> service =
+ GeckoMediaPluginService::GetGeckoMediaPluginService();
+ return ((*service).*Getter)(nullptr, &tags, aNodeId, Move(aCallback));
+ }
+
+protected:
+ GMPTestMonitor& mMonitor;
+};
+
+typedef RunTestGMPVideoCodec<GMPVideoDecoderProxy,
+ GetGMPVideoDecoderCallback,
+ &GeckoMediaPluginService::GetGMPVideoDecoder>
+ RunTestGMPVideoDecoder;
+typedef RunTestGMPVideoCodec<GMPVideoEncoderProxy,
+ GetGMPVideoEncoderCallback,
+ &GeckoMediaPluginService::GetGMPVideoEncoder>
+ RunTestGMPVideoEncoder;
+
+void
+GMPTestRunner::RunTestGMPTestCodec1(GMPTestMonitor& aMonitor)
+{
+ RunTestGMPVideoDecoder::Run(aMonitor, NS_LITERAL_CSTRING("o"));
+}
+
+void
+GMPTestRunner::RunTestGMPTestCodec2(GMPTestMonitor& aMonitor)
+{
+ RunTestGMPVideoDecoder::Run(aMonitor, NS_LITERAL_CSTRING(""));
+}
+
+void
+GMPTestRunner::RunTestGMPTestCodec3(GMPTestMonitor& aMonitor)
+{
+ RunTestGMPVideoEncoder::Run(aMonitor, NS_LITERAL_CSTRING(""));
+}
+
+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, 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, 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, NS_LITERAL_CSTRING("origin1"), NS_LITERAL_CSTRING("origin2"));
+}
+
+void
+GMPTestRunner::RunTestGMPCrossOrigin2(GMPTestMonitor& aMonitor)
+{
+ RunTestGMPVideoEncoderCrossOrigin::Run(
+ aMonitor, NS_LITERAL_CSTRING("origin1"), NS_LITERAL_CSTRING("origin2"));
+}
+
+void
+GMPTestRunner::RunTestGMPCrossOrigin3(GMPTestMonitor& aMonitor)
+{
+ RunTestGMPVideoDecoderCrossOrigin::Run(
+ aMonitor, NS_LITERAL_CSTRING("origin1"), NS_LITERAL_CSTRING("origin1"));
+}
+
+void
+GMPTestRunner::RunTestGMPCrossOrigin4(GMPTestMonitor& aMonitor)
+{
+ RunTestGMPVideoEncoderCrossOrigin::Run(
+ aMonitor, NS_LITERAL_CSTRING("origin1"), NS_LITERAL_CSTRING("origin1"));
+}
+
+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<nsISimpleEnumerator> iter;
+ nsresult rv = aPath->GetDirectoryEntries(getter_AddRefs(iter));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ bool hasMore = false;
+ while (NS_SUCCEEDED(iter->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsISupports> supports;
+ rv = iter->GetNext(getter_AddRefs(supports));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ nsCOMPtr<nsIFile> entry(do_QueryInterface(supports, &rv));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ aDirIter(entry);
+ }
+ return NS_OK;
+}
+
+/**
+ * Enumerate files under $profileDir/gmp/$platform/gmp-fake/$aDir/ (non-recursive).
+ */
+template<typename T>
+static nsresult
+EnumerateGMPStorageDir(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(NS_LITERAL_STRING("gmp-fake"));
+ 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() {}
+ 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)
+ : 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 ClearGMPStorageTask : public nsIRunnable
+ , public nsIObserver {
+public:
+ ClearGMPStorageTask(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 ~ClearGMPStorageTask() {}
+ nsCOMPtr<nsIRunnable> mContinuation;
+ nsCOMPtr<nsIThread> mTarget;
+ const PRTime mSince;
+};
+
+NS_IMPL_ISUPPORTS(ClearGMPStorageTask, nsIRunnable, nsIObserver)
+
+static void
+ClearGMPStorage(already_AddRefed<nsIRunnable> aContinuation,
+ nsIThread* aTarget, PRTime aSince = -1)
+{
+ RefPtr<ClearGMPStorageTask> task(
+ new ClearGMPStorageTask(Move(aContinuation), aTarget, aSince));
+ NS_DispatchToMainThread(task, NS_DISPATCH_NORMAL);
+}
+
+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 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));
+ // We rely on the fact that the GetNodeId implementation for
+ // GeckoMediaPluginServiceParent is synchronous.
+ nsresult rv = service->GetNodeId(aOrigin,
+ aTopLevelOrigin,
+ NS_LITERAL_STRING("gmp-fake"),
+ aInPBMode,
+ Move(callback));
+ EXPECT_TRUE(NS_SUCCEEDED(rv) && NS_SUCCEEDED(result));
+ return nodeId;
+}
+
+static bool
+IsGMPStorageIsEmpty()
+{
+ 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 GMPStorageTest : public GMPDecryptorProxyCallback
+{
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPStorageTest)
+
+ void DoTest(void (GMPStorageTest::*aTestMethod)()) {
+ EnsureNSSInitializedChromeOrContent();
+ nsCOMPtr<nsIThread> thread(GetGMPThread());
+ ClearGMPStorage(NewRunnableMethod(this, aTestMethod), thread);
+ AwaitFinished();
+ }
+
+ GMPStorageTest()
+ : mDecryptor(nullptr)
+ , mMonitor("GMPStorageTest")
+ , mFinished(false)
+ {
+ }
+
+ void
+ Update(const nsCString& aMessage)
+ {
+ nsTArray<uint8_t> msg;
+ msg.AppendElements(aMessage.get(), aMessage.Length());
+ mDecryptor->UpdateSession(1, NS_LITERAL_CSTRING("fake-session-id"), msg);
+ }
+
+ void TestGetNodeId()
+ {
+ AssertIsOnGMPThread();
+
+ EXPECT_TRUE(IsGMPStorageIsEmpty());
+
+ const nsString origin1 = NS_LITERAL_STRING("http://example1.com");
+ const nsString origin2 = NS_LITERAL_STRING("http://example2.org");
+
+ 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(IsGMPStorageIsEmpty());
+
+ 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());
+ ClearGMPStorage(NewRunnableMethod<nsCString>(
+ this, &GMPStorageTest::TestGetNodeId_Continuation, nodeId1), thread);
+ }
+
+ void TestGetNodeId_Continuation(nsCString aNodeId1) {
+ EXPECT_TRUE(IsGMPStorageIsEmpty());
+
+ // Once we clear storage, the node ids generated for the same origin-pair
+ // should be different.
+ const nsString origin1 = NS_LITERAL_STRING("http://example1.com");
+ const nsString origin2 = NS_LITERAL_STRING("http://example2.org");
+ nsCString nodeId3 = GetNodeId(origin1, origin2, false);
+ EXPECT_TRUE(!aNodeId1.Equals(nodeId3));
+
+ SetFinished();
+ }
+
+ class CreateDecryptorDone : public GetGMPDecryptorCallback
+ {
+ public:
+ explicit CreateDecryptorDone(GMPStorageTest* aRunner)
+ : mRunner(aRunner)
+ {
+ }
+
+ void Done(GMPDecryptorProxy* aDecryptor) override
+ {
+ mRunner->mDecryptor = aDecryptor;
+ EXPECT_TRUE(!!mRunner->mDecryptor);
+
+ if (mRunner->mDecryptor) {
+ mRunner->mDecryptor->Init(mRunner, false, true);
+ }
+ }
+
+ private:
+ RefPtr<GMPStorageTest> mRunner;
+ };
+
+ void CreateDecryptor(const nsCString& aNodeId,
+ const nsCString& aUpdate)
+ {
+ nsTArray<nsCString> updates;
+ updates.AppendElement(aUpdate);
+ nsCOMPtr<nsIRunnable> continuation(new Updates(this, Move(updates)));
+ CreateDecryptor(aNodeId, continuation);
+ }
+
+ void CreateDecryptor(const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin,
+ bool aInPBMode,
+ const nsCString& aUpdate)
+ {
+ nsTArray<nsCString> updates;
+ updates.AppendElement(aUpdate);
+ CreateDecryptor(aOrigin, aTopLevelOrigin, aInPBMode, Move(updates));
+ }
+ class Updates : public Runnable
+ {
+ public:
+ Updates(GMPStorageTest* aRunner, nsTArray<nsCString>&& aUpdates)
+ : mRunner(aRunner),
+ mUpdates(Move(aUpdates))
+ {
+ }
+
+ NS_IMETHOD Run() override
+ {
+ for (auto& update : mUpdates) {
+ mRunner->Update(update);
+ }
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<GMPStorageTest> mRunner;
+ nsTArray<nsCString> mUpdates;
+ };
+ void CreateDecryptor(const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin,
+ bool aInPBMode,
+ nsTArray<nsCString>&& aUpdates) {
+ nsCOMPtr<nsIRunnable> updates(new Updates(this, Move(aUpdates)));
+ CreateDecryptor(GetNodeId(aOrigin, aTopLevelOrigin, aInPBMode), updates);
+ }
+
+ void CreateDecryptor(const nsCString& aNodeId,
+ nsIRunnable* aContinuation) {
+ RefPtr<GeckoMediaPluginService> service =
+ GeckoMediaPluginService::GetGeckoMediaPluginService();
+ EXPECT_TRUE(service);
+
+ mNodeId = aNodeId;
+ EXPECT_TRUE(!mNodeId.IsEmpty());
+
+ nsTArray<nsCString> tags;
+ tags.AppendElement(NS_LITERAL_CSTRING("fake"));
+
+ UniquePtr<GetGMPDecryptorCallback> callback(
+ new CreateDecryptorDone(this));
+
+ // Continue after the OnSetDecryptorId message, so that we don't
+ // get warnings in the async shutdown tests due to receiving the
+ // SetDecryptorId message after we've started shutdown.
+ mSetDecryptorIdContinuation = aContinuation;
+
+ nsresult rv =
+ service->GetGMPDecryptor(nullptr, &tags, mNodeId, Move(callback));
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+ }
+
+ void TestBasicStorage() {
+ AssertIsOnGMPThread();
+ EXPECT_TRUE(IsGMPStorageIsEmpty());
+
+ 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(NS_LITERAL_CSTRING("test-storage complete"),
+ NewRunnableMethod(this, &GMPStorageTest::SetFinished));
+
+ CreateDecryptor(NS_LITERAL_STRING("http://example1.com"),
+ NS_LITERAL_STRING("http://example2.com"),
+ false,
+ NS_LITERAL_CSTRING("test-storage"));
+ }
+
+ /**
+ * 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(IsGMPStorageIsEmpty());
+
+ // Generate storage data for some site.
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ this, &GMPStorageTest::TestForgetThisSite_AnotherSite);
+ Expect(NS_LITERAL_CSTRING("test-storage complete"), r.forget());
+
+ CreateDecryptor(NS_LITERAL_STRING("http://example1.com"),
+ NS_LITERAL_STRING("http://example2.com"),
+ false,
+ NS_LITERAL_CSTRING("test-storage"));
+ }
+
+ void TestForgetThisSite_AnotherSite() {
+ Shutdown();
+
+ // Generate storage data for another site.
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ this, &GMPStorageTest::TestForgetThisSite_CollectSiteInfo);
+ Expect(NS_LITERAL_CSTRING("test-storage complete"), r.forget());
+
+ CreateDecryptor(NS_LITERAL_STRING("http://example3.com"),
+ NS_LITERAL_STRING("http://example4.com"),
+ false,
+ NS_LITERAL_CSTRING("test-storage"));
+ }
+
+ 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;
+
+ nsAutoPtr<NodeInfo> siteInfo(
+ new NodeInfo(NS_LITERAL_CSTRING("http://example1.com"),
+ pattern));
+ // Collect nodeIds that are expected to remain for later comparison.
+ EnumerateGMPStorageDir(NS_LITERAL_CSTRING("id"), NodeIdCollector(siteInfo));
+ // Invoke "Forget this site" on the main thread.
+ NS_DispatchToMainThread(NewRunnableMethod<nsAutoPtr<NodeInfo>>(
+ this, &GMPStorageTest::TestForgetThisSite_Forget, siteInfo));
+ }
+
+ void TestForgetThisSite_Forget(nsAutoPtr<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<nsAutoPtr<NodeInfo>>(
+ this, &GMPStorageTest::TestForgetThisSite_Verify, aSiteInfo);
+ thread->Dispatch(r, NS_DISPATCH_NORMAL);
+
+ nsCOMPtr<nsIRunnable> f = NewRunnableMethod(
+ this, &GMPStorageTest::SetFinished);
+ thread->Dispatch(f, NS_DISPATCH_NORMAL);
+ }
+
+ class NodeIdVerifier {
+ public:
+ explicit NodeIdVerifier(const NodeInfo* aInfo)
+ : mNodeInfo(aInfo)
+ , mExpectedRemainingNodeIds(aInfo->expectedRemainingNodeIds) {}
+ 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) {}
+ 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(nsAutoPtr<NodeInfo> aSiteInfo) {
+ nsresult rv = EnumerateGMPStorageDir(
+ NS_LITERAL_CSTRING("id"), NodeIdVerifier(aSiteInfo));
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+ rv = EnumerateGMPStorageDir(
+ NS_LITERAL_CSTRING("storage"), StorageVerifier(aSiteInfo));
+ 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(IsGMPStorageIsEmpty());
+
+ // Generate storage data for some site.
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ this, &GMPStorageTest::TestClearRecentHistory1_Clear);
+ Expect(NS_LITERAL_CSTRING("test-storage complete"), r.forget());
+
+ CreateDecryptor(NS_LITERAL_STRING("http://example1.com"),
+ NS_LITERAL_STRING("http://example2.com"),
+ false,
+ NS_LITERAL_CSTRING("test-storage"));
+}
+
+ /**
+ * 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(IsGMPStorageIsEmpty());
+
+ // Generate storage data for some site.
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ this, &GMPStorageTest::TestClearRecentHistory2_Clear);
+ Expect(NS_LITERAL_CSTRING("test-storage complete"), r.forget());
+
+ CreateDecryptor(NS_LITERAL_STRING("http://example1.com"),
+ NS_LITERAL_STRING("http://example2.com"),
+ false,
+ NS_LITERAL_CSTRING("test-storage"));
+ }
+
+ /**
+ * 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(IsGMPStorageIsEmpty());
+
+ // Generate storage data for some site.
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ this, &GMPStorageTest::TestClearRecentHistory3_Clear);
+ Expect(NS_LITERAL_CSTRING("test-storage complete"), r.forget());
+
+ CreateDecryptor(NS_LITERAL_STRING("http://example1.com"),
+ NS_LITERAL_STRING("http://example2.com"),
+ false,
+ NS_LITERAL_CSTRING("test-storage"));
+ }
+
+ 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 = EnumerateGMPStorageDir(NS_LITERAL_CSTRING("id"), f);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ this, &GMPStorageTest::TestClearRecentHistory_CheckEmpty);
+ nsCOMPtr<nsIThread> t(GetGMPThread());
+ ClearGMPStorage(r.forget(), t, f.GetResult());
+ }
+
+ void TestClearRecentHistory2_Clear() {
+ MaxMTimeFinder f;
+ nsresult rv = EnumerateGMPStorageDir(NS_LITERAL_CSTRING("storage"), f);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ this, &GMPStorageTest::TestClearRecentHistory_CheckEmpty);
+ nsCOMPtr<nsIThread> t(GetGMPThread());
+ ClearGMPStorage(r.forget(), t, f.GetResult());
+ }
+
+ void TestClearRecentHistory3_Clear() {
+ MaxMTimeFinder f;
+ nsresult rv = EnumerateGMPStorageDir(NS_LITERAL_CSTRING("storage"), f);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ this, &GMPStorageTest::TestClearRecentHistory_CheckNonEmpty);
+ nsCOMPtr<nsIThread> t(GetGMPThread());
+ ClearGMPStorage(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 = EnumerateGMPStorageDir(NS_LITERAL_CSTRING("id"), 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 = EnumerateGMPStorageDir(NS_LITERAL_CSTRING("storage"), 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 = EnumerateGMPStorageDir(NS_LITERAL_CSTRING("id"), 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 = EnumerateGMPStorageDir(NS_LITERAL_CSTRING("storage"), 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(!mDecryptor);
+
+ // 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(this,
+ &GMPStorageTest::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(NS_LITERAL_STRING("http://example3.com"),
+ NS_LITERAL_STRING("http://example4.com"),
+ 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(NS_LITERAL_CSTRING("retrieve crossOriginTestRecordId succeeded (length 0 bytes)"),
+ NewRunnableMethod(this, &GMPStorageTest::SetFinished));
+
+ CreateDecryptor(NS_LITERAL_STRING("http://example5.com"),
+ NS_LITERAL_STRING("http://example6.com"),
+ false,
+ NS_LITERAL_CSTRING("retrieve crossOriginTestRecordId"));
+ }
+
+ 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(this,
+ &GMPStorageTest::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(NS_LITERAL_STRING("http://pb1.com"),
+ NS_LITERAL_STRING("http://pb2.com"),
+ true,
+ NS_LITERAL_CSTRING("store pbdata test-pb-data"));
+ }
+
+ void TestPBStorage_RecordStoredContinuation() {
+ Shutdown();
+
+ Expect(NS_LITERAL_CSTRING("retrieve pbdata succeeded (length 12 bytes)"),
+ NewRunnableMethod(this,
+ &GMPStorageTest::TestPBStorage_RecordRetrievedContinuation));
+
+ CreateDecryptor(NS_LITERAL_STRING("http://pb1.com"),
+ NS_LITERAL_STRING("http://pb2.com"),
+ true,
+ NS_LITERAL_CSTRING("retrieve pbdata"));
+ }
+
+ void TestPBStorage_RecordRetrievedContinuation() {
+ Shutdown();
+ SimulatePBModeExit();
+
+ Expect(NS_LITERAL_CSTRING("retrieve pbdata succeeded (length 0 bytes)"),
+ NewRunnableMethod(this,
+ &GMPStorageTest::SetFinished));
+
+ CreateDecryptor(NS_LITERAL_STRING("http://pb1.com"),
+ NS_LITERAL_STRING("http://pb2.com"),
+ true,
+ NS_LITERAL_CSTRING("retrieve pbdata"));
+ }
+
+ void NextAsyncShutdownTimeoutTest(nsIRunnable* aContinuation)
+ {
+ if (mDecryptor) {
+ Update(NS_LITERAL_CSTRING("shutdown-mode timeout"));
+ Shutdown();
+ }
+ nsCOMPtr<nsIThread> thread(GetGMPThread());
+ thread->Dispatch(aContinuation, NS_DISPATCH_NORMAL);
+ }
+
+ void CreateAsyncShutdownTimeoutGMP(const nsAString& aOrigin1,
+ const nsAString& aOrigin2,
+ void (GMPStorageTest::*aCallback)()) {
+ nsCOMPtr<nsIRunnable> continuation(
+ NewRunnableMethod<nsCOMPtr<nsIRunnable>>(
+ this,
+ &GMPStorageTest::NextAsyncShutdownTimeoutTest,
+ NewRunnableMethod(this, aCallback)));
+
+ CreateDecryptor(GetNodeId(aOrigin1, aOrigin2, false), continuation);
+ }
+
+ void TestAsyncShutdownTimeout() {
+ // Create decryptors that timeout in their async shutdown.
+ // If the gtest hangs on shutdown, test fails!
+ CreateAsyncShutdownTimeoutGMP(NS_LITERAL_STRING("http://example7.com"),
+ NS_LITERAL_STRING("http://example8.com"),
+ &GMPStorageTest::TestAsyncShutdownTimeout2);
+ };
+
+ void TestAsyncShutdownTimeout2() {
+ CreateAsyncShutdownTimeoutGMP(NS_LITERAL_STRING("http://example9.com"),
+ NS_LITERAL_STRING("http://example10.com"),
+ &GMPStorageTest::TestAsyncShutdownTimeout3);
+ };
+
+ void TestAsyncShutdownTimeout3() {
+ CreateAsyncShutdownTimeoutGMP(NS_LITERAL_STRING("http://example11.com"),
+ NS_LITERAL_STRING("http://example12.com"),
+ &GMPStorageTest::SetFinished);
+ };
+
+ void TestAsyncShutdownStorage() {
+ // Instruct the GMP to write a token (the current timestamp, so it's
+ // unique) during async shutdown, then shutdown the plugin, re-create
+ // it, and check that the token was successfully stored.
+ auto t = time(0);
+ nsCString update("shutdown-mode token ");
+ nsCString token;
+ token.AppendInt((int64_t)t);
+ update.Append(token);
+
+ // Wait for a response from the GMP, so we know it's had time to receive
+ // the token.
+ nsCString response("shutdown-token received ");
+ response.Append(token);
+ Expect(response, NewRunnableMethod<nsCString>(this,
+ &GMPStorageTest::TestAsyncShutdownStorage_ReceivedShutdownToken, token));
+
+ // Test that a GMP can write to storage during shutdown, and retrieve
+ // that written data in a subsequent session.
+ CreateDecryptor(NS_LITERAL_STRING("http://example13.com"),
+ NS_LITERAL_STRING("http://example14.com"),
+ false,
+ update);
+ }
+
+ void TestAsyncShutdownStorage_ReceivedShutdownToken(const nsCString& aToken) {
+ ShutdownThen(NewRunnableMethod<nsCString>(this,
+ &GMPStorageTest::TestAsyncShutdownStorage_AsyncShutdownComplete, aToken));
+ }
+
+ void TestAsyncShutdownStorage_AsyncShutdownComplete(const nsCString& aToken) {
+ // Create a new instance of the plugin, retrieve the token written
+ // during shutdown and verify it is correct.
+ nsCString response("retrieved shutdown-token ");
+ response.Append(aToken);
+ Expect(response,
+ NewRunnableMethod(this, &GMPStorageTest::SetFinished));
+
+ CreateDecryptor(NS_LITERAL_STRING("http://example13.com"),
+ NS_LITERAL_STRING("http://example14.com"),
+ false,
+ NS_LITERAL_CSTRING("retrieve-shutdown-token"));
+ }
+
+#if defined(XP_WIN)
+ void TestOutputProtection() {
+ Shutdown();
+
+ Expect(NS_LITERAL_CSTRING("OP tests completed"),
+ NewRunnableMethod(this, &GMPStorageTest::SetFinished));
+
+ CreateDecryptor(NS_LITERAL_STRING("http://example15.com"),
+ NS_LITERAL_STRING("http://example16.com"),
+ false,
+ NS_LITERAL_CSTRING("test-op-apis"));
+ }
+#endif
+
+ void TestPluginVoucher() {
+ Expect(NS_LITERAL_CSTRING("retrieved plugin-voucher: gmp-fake placeholder voucher"),
+ NewRunnableMethod(this, &GMPStorageTest::SetFinished));
+
+ CreateDecryptor(NS_LITERAL_STRING("http://example17.com"),
+ NS_LITERAL_STRING("http://example18.com"),
+ false,
+ NS_LITERAL_CSTRING("retrieve-plugin-voucher"));
+ }
+
+ void TestGetRecordNamesInMemoryStorage() {
+ TestGetRecordNames(true);
+ }
+
+ nsCString mRecordNames;
+
+ void AppendIntPadded(nsACString& aString, uint32_t aInt) {
+ if (aInt > 0 && aInt < 10) {
+ aString.AppendLiteral("0");
+ }
+ aString.AppendInt(aInt);
+ }
+
+ void TestGetRecordNames(bool aPrivateBrowsing) {
+ // Create a number of records of different names.
+ const uint32_t num = 100;
+ nsTArray<nsCString> updates(num);
+ for (uint32_t i = 0; i < num; i++) {
+ nsAutoCString response;
+ response.AppendLiteral("stored data");
+ AppendIntPadded(response, i);
+ response.AppendLiteral(" test-data");
+ AppendIntPadded(response, i);
+
+ if (i != 0) {
+ mRecordNames.AppendLiteral(",");
+ }
+ mRecordNames.AppendLiteral("data");
+ AppendIntPadded(mRecordNames, i);
+
+ nsCString& update = *updates.AppendElement();
+ update.AppendLiteral("store data");
+ AppendIntPadded(update, i);
+ update.AppendLiteral(" test-data");
+ AppendIntPadded(update, i);
+
+ nsCOMPtr<nsIRunnable> continuation;
+ if (i + 1 == num) {
+ continuation =
+ NewRunnableMethod(this, &GMPStorageTest::TestGetRecordNames_QueryNames);
+ }
+ Expect(response, continuation.forget());
+ }
+
+ CreateDecryptor(NS_LITERAL_STRING("http://foo.com"),
+ NS_LITERAL_STRING("http://bar.com"),
+ aPrivateBrowsing,
+ Move(updates));
+ }
+
+ void TestGetRecordNames_QueryNames() {
+ nsCString response("record-names ");
+ response.Append(mRecordNames);
+ Expect(response,
+ NewRunnableMethod(this, &GMPStorageTest::SetFinished));
+ Update(NS_LITERAL_CSTRING("retrieve-record-names"));
+ }
+
+ void GetRecordNamesPersistentStorage() {
+ TestGetRecordNames(false);
+ }
+
+ void TestLongRecordNames() {
+ NS_NAMED_LITERAL_CSTRING(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_NAMED_LITERAL_CSTRING(data, "Just_some_arbitrary_data.");
+
+ 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(this, &GMPStorageTest::SetFinished));
+
+ nsCString update("store ");
+ update.Append(longRecordName);
+ update.AppendLiteral(" ");
+ update.Append(data);
+ CreateDecryptor(NS_LITERAL_STRING("http://fuz.com"),
+ NS_LITERAL_STRING("http://baz.com"),
+ false,
+ update);
+ }
+
+ void TestNodeId() {
+ // Calculate the nodeId, and the device bound nodeId. Start a GMP, and
+ // have it return the device bound nodeId that it's been passed. Assert
+ // they have the same value.
+ const nsString origin = NS_LITERAL_STRING("http://example-fuz-baz.com");
+ nsCString originSalt1 = GetNodeId(origin, origin, false);
+
+ nsCString salt = originSalt1;
+ std::string nodeId;
+ EXPECT_TRUE(CalculateGMPDeviceId(salt.BeginWriting(), salt.Length(), nodeId));
+
+ std::string expected = "node-id " + nodeId;
+ Expect(nsDependentCString(expected.c_str()), NewRunnableMethod(this, &GMPStorageTest::SetFinished));
+
+ CreateDecryptor(originSalt1,
+ NS_LITERAL_CSTRING("retrieve-node-id"));
+ }
+
+ void Expect(const nsCString& aMessage, already_AddRefed<nsIRunnable> aContinuation) {
+ mExpected.AppendElement(ExpectedMessage(aMessage, Move(aContinuation)));
+ }
+
+ void AwaitFinished() {
+ while (!mFinished) {
+ NS_ProcessNextEvent(nullptr, true);
+ }
+ mFinished = false;
+ }
+
+ void ShutdownThen(already_AddRefed<nsIRunnable> aContinuation) {
+ EXPECT_TRUE(!!mDecryptor);
+ if (!mDecryptor) {
+ return;
+ }
+ EXPECT_FALSE(mNodeId.IsEmpty());
+ RefPtr<GMPShutdownObserver> task(
+ new GMPShutdownObserver(NewRunnableMethod(this, &GMPStorageTest::Shutdown),
+ Move(aContinuation), mNodeId));
+ NS_DispatchToMainThread(task, NS_DISPATCH_NORMAL);
+ }
+
+ void Shutdown() {
+ if (mDecryptor) {
+ mDecryptor->Close();
+ mDecryptor = nullptr;
+ mNodeId = EmptyCString();
+ }
+ }
+
+ void Dummy() {
+ }
+
+ void SetFinished() {
+ mFinished = true;
+ Shutdown();
+ NS_DispatchToMainThread(NewRunnableMethod(this, &GMPStorageTest::Dummy));
+ }
+
+ void SessionMessage(const nsCString& aSessionId,
+ mozilla::dom::MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) override
+ {
+ 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 SetDecryptorId(uint32_t aId) override
+ {
+ if (!mSetDecryptorIdContinuation) {
+ return;
+ }
+ nsCOMPtr<nsIThread> thread(GetGMPThread());
+ thread->Dispatch(mSetDecryptorIdContinuation, NS_DISPATCH_NORMAL);
+ mSetDecryptorIdContinuation = nullptr;
+ }
+
+ void SetSessionId(uint32_t aCreateSessionToken,
+ const nsCString& aSessionId) override { }
+ void ResolveLoadSessionPromise(uint32_t aPromiseId,
+ bool aSuccess) override {}
+ void ResolvePromise(uint32_t aPromiseId) override {}
+ void RejectPromise(uint32_t aPromiseId,
+ nsresult aException,
+ const nsCString& aSessionId) override { }
+ void ExpirationChange(const nsCString& aSessionId,
+ UnixTime aExpiryTime) override {}
+ void SessionClosed(const nsCString& aSessionId) override {}
+ void SessionError(const nsCString& aSessionId,
+ nsresult aException,
+ uint32_t aSystemCode,
+ const nsCString& aMessage) override {}
+ void Decrypted(uint32_t aId,
+ mozilla::DecryptStatus aResult,
+ const nsTArray<uint8_t>& aDecryptedData) override { }
+
+ void BatchedKeyStatusChanged(const nsCString& aSessionId,
+ const nsTArray<CDMKeyInfo>& aKeyInfos) override { }
+
+ void Terminated() override {
+ if (mDecryptor) {
+ mDecryptor->Close();
+ mDecryptor = nullptr;
+ }
+ }
+
+private:
+ ~GMPStorageTest() { }
+
+ 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;
+
+ GMPDecryptorProxy* mDecryptor;
+ Monitor mMonitor;
+ Atomic<bool> mFinished;
+ nsCString mNodeId;
+};
+
+void
+GMPTestRunner::DoTest(void (GMPTestRunner::*aTestMethod)(GMPTestMonitor&))
+{
+ nsCOMPtr<nsIThread> thread(GetGMPThread());
+
+ GMPTestMonitor monitor;
+ thread->Dispatch(NewRunnableMethod<GMPTestMonitor&>(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);
+}
+
+TEST(GeckoMediaPlugins, GMPStorageGetNodeId) {
+ RefPtr<GMPStorageTest> runner = new GMPStorageTest();
+ runner->DoTest(&GMPStorageTest::TestGetNodeId);
+}
+
+TEST(GeckoMediaPlugins, GMPStorageBasic) {
+ RefPtr<GMPStorageTest> runner = new GMPStorageTest();
+ runner->DoTest(&GMPStorageTest::TestBasicStorage);
+}
+
+TEST(GeckoMediaPlugins, GMPStorageForgetThisSite) {
+ RefPtr<GMPStorageTest> runner = new GMPStorageTest();
+ runner->DoTest(&GMPStorageTest::TestForgetThisSite);
+}
+
+TEST(GeckoMediaPlugins, GMPStorageClearRecentHistory1) {
+ RefPtr<GMPStorageTest> runner = new GMPStorageTest();
+ runner->DoTest(&GMPStorageTest::TestClearRecentHistory1);
+}
+
+TEST(GeckoMediaPlugins, GMPStorageClearRecentHistory2) {
+ RefPtr<GMPStorageTest> runner = new GMPStorageTest();
+ runner->DoTest(&GMPStorageTest::TestClearRecentHistory2);
+}
+
+TEST(GeckoMediaPlugins, GMPStorageClearRecentHistory3) {
+ RefPtr<GMPStorageTest> runner = new GMPStorageTest();
+ runner->DoTest(&GMPStorageTest::TestClearRecentHistory3);
+}
+
+TEST(GeckoMediaPlugins, GMPStorageCrossOrigin) {
+ RefPtr<GMPStorageTest> runner = new GMPStorageTest();
+ runner->DoTest(&GMPStorageTest::TestCrossOriginStorage);
+}
+
+TEST(GeckoMediaPlugins, GMPStoragePrivateBrowsing) {
+ RefPtr<GMPStorageTest> runner = new GMPStorageTest();
+ runner->DoTest(&GMPStorageTest::TestPBStorage);
+}
+
+TEST(GeckoMediaPlugins, GMPStorageAsyncShutdownTimeout) {
+ RefPtr<GMPStorageTest> runner = new GMPStorageTest();
+ runner->DoTest(&GMPStorageTest::TestAsyncShutdownTimeout);
+}
+
+TEST(GeckoMediaPlugins, GMPStorageAsyncShutdownStorage) {
+ RefPtr<GMPStorageTest> runner = new GMPStorageTest();
+ runner->DoTest(&GMPStorageTest::TestAsyncShutdownStorage);
+}
+
+TEST(GeckoMediaPlugins, GMPPluginVoucher) {
+ RefPtr<GMPStorageTest> runner = new GMPStorageTest();
+ runner->DoTest(&GMPStorageTest::TestPluginVoucher);
+}
+
+#if defined(XP_WIN)
+TEST(GeckoMediaPlugins, GMPOutputProtection) {
+ // Output Protection is not available pre-Vista.
+ if (!IsVistaOrLater()) {
+ return;
+ }
+
+ RefPtr<GMPStorageTest> runner = new GMPStorageTest();
+ runner->DoTest(&GMPStorageTest::TestOutputProtection);
+}
+#endif
+
+TEST(GeckoMediaPlugins, GMPStorageGetRecordNamesInMemoryStorage) {
+ RefPtr<GMPStorageTest> runner = new GMPStorageTest();
+ runner->DoTest(&GMPStorageTest::TestGetRecordNamesInMemoryStorage);
+}
+
+TEST(GeckoMediaPlugins, GMPStorageGetRecordNamesPersistentStorage) {
+ RefPtr<GMPStorageTest> runner = new GMPStorageTest();
+ runner->DoTest(&GMPStorageTest::GetRecordNamesPersistentStorage);
+}
+
+TEST(GeckoMediaPlugins, GMPStorageLongRecordNames) {
+ RefPtr<GMPStorageTest> runner = new GMPStorageTest();
+ runner->DoTest(&GMPStorageTest::TestLongRecordNames);
+}
+
+TEST(GeckoMediaPlugins, GMPNodeId) {
+ RefPtr<GMPStorageTest> runner = new GMPStorageTest();
+ runner->DoTest(&GMPStorageTest::TestNodeId);
+}
diff --git a/dom/media/gtest/TestGMPRemoveAndDelete.cpp b/dom/media/gtest/TestGMPRemoveAndDelete.cpp
new file mode 100644
index 0000000000..4ac92e34e5
--- /dev/null
+++ b/dom/media/gtest/TestGMPRemoveAndDelete.cpp
@@ -0,0 +1,491 @@
+/* -*- 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 "GMPTestMonitor.h"
+#include "gmp-api/gmp-video-host.h"
+#include "gtest/gtest.h"
+#include "mozilla/Services.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIObserverService.h"
+#include "GMPVideoDecoderProxy.h"
+#include "GMPServiceParent.h"
+#include "GMPService.h"
+#include "GMPUtils.h"
+#include "mozilla/StaticPtr.h"
+#include "MediaPrefs.h"
+
+#define GMP_DIR_NAME NS_LITERAL_STRING("gmp-fakeopenh264")
+#define GMP_OLD_VERSION NS_LITERAL_STRING("1.0")
+#define GMP_NEW_VERSION NS_LITERAL_STRING("1.1")
+
+#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 = EmptyCString());
+ 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(NS_LITERAL_CSTRING("thisOrigin")));
+
+ // 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(NS_LITERAL_CSTRING("thisOrigin")));
+
+ // 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(NS_LITERAL_CSTRING("thisOrigin")));
+
+ // 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(NS_LITERAL_CSTRING("thisOrigin")));
+
+ 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()
+{
+ // Initialize media preferences.
+ MediaPrefs::GetSingleton();
+ 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.
+ RefPtr<AbstractThread> thread(GetServiceParent()->GetAbstractGMPThread());
+ 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**>(
+ 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>(
+ 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(NS_LITERAL_CSTRING("h264"));
+ tags.AppendElement(NS_LITERAL_CSTRING("fake"));
+
+ 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, Move(cb)))) {
+ mTestMonitor.SetFinished();
+ }
+}
+
+void
+GMPRemoveTest::CloseVideoDecoder()
+{
+ mGMPThread->Dispatch(
+ NewNonOwningRunnableMethod(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(this, &GMPRemoveTest::gmp_Decode),
+ NS_DISPATCH_NORMAL);
+
+ mTestMonitor.AwaitFinished();
+ return mDecodeResult;
+}
+
+void
+GMPRemoveTest::gmp_Decode()
+{
+ // from gmp-fake.cpp
+ struct EncodedFrame {
+ uint32_t length_;
+ 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_;
+ };
+
+ 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->magic_ = 0x4652414d;
+ frameData->width_ = frameData->height_ = 16;
+
+ nsTArray<uint8_t> empty;
+ nsresult rv = mDecoder->Decode(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..75d7eba53e
--- /dev/null
+++ b/dom/media/gtest/TestGMPUtils.cpp
@@ -0,0 +1,61 @@
+/* -*- 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 "nsString.h"
+#include "MediaPrefs.h"
+
+#include <string>
+#include <vector>
+
+using namespace std;
+using namespace mozilla;
+
+void TestSplitAt(const char* aInput,
+ const char* aDelims,
+ size_t aNumExpectedTokens,
+ const char* aExpectedTokens[])
+{
+ // Initialize media preferences.
+ MediaPrefs::GetSingleton();
+ 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, GMPUtils) {
+ {
+ 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);
+ }
+}
diff --git a/dom/media/gtest/TestIntervalSet.cpp b/dom/media/gtest/TestIntervalSet.cpp
new file mode 100644
index 0000000000..b054ba1d24
--- /dev/null
+++ b/dom/media/gtest/TestIntervalSet.cpp
@@ -0,0 +1,821 @@
+/* -*- 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/dom/TimeRanges.h"
+#include "TimeUnits.h"
+#include "Intervals.h"
+#include <algorithm>
+#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::Microseconds startus(aStart);
+ media::TimeUnit start(startus);
+ media::TimeUnit end;
+ // operator= test
+ end = media::Microseconds(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 media::Microseconds start(1);
+ const media::Microseconds end(2);
+ const media::Microseconds 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.mValue, end.mValue);
+
+ media::TimeIntervals blah1(test1);
+ media::TimeIntervals blah2(blah1);
+ media::TimeIntervals blah3 = blah1 + test1;
+ media::TimeIntervals blah4 = test1 + blah1;
+ media::TimeIntervals blah5 = CreateTimeIntervals(start.mValue, end.mValue);
+ (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_EQ(0u, i0.Length()); // 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(IntIntervals aI1,
+ 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.ToTimeRanges(tr);
+ 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
+ media::TimeIntervals i1(tr);
+ CheckTimeRanges(tr, i1);
+
+ // static FromTimeRanges
+ media::TimeIntervals i2 = media::TimeIntervals::FromTimeRanges(tr);
+ CheckTimeRanges(tr, i2);
+
+ media::TimeIntervals i3;
+ // operator=(TimeRanges*)
+ i3 = tr;
+ CheckTimeRanges(tr, i3);
+
+ // operator= test
+ i1 = tr.get();
+ CheckTimeRanges(tr, i1);
+}
+
+TEST(IntervalSet, TimeRangesMicroseconds)
+{
+ media::TimeIntervals i0;
+
+ // Test media::Microseconds and TimeUnit interchangeability (compilation only)
+ media::TimeUnit time1{media::Microseconds(5)};
+ media::Microseconds microseconds(5);
+ media::TimeUnit time2 = media::TimeUnit(microseconds);
+ EXPECT_EQ(time1, time2);
+
+ i0 += media::TimeInterval(media::Microseconds(20), media::Microseconds(25));
+ i0 += media::TimeInterval(media::Microseconds(40), media::Microseconds(60));
+ i0 += media::TimeInterval(media::Microseconds(5), media::Microseconds(10));
+
+ media::TimeIntervals i1;
+ i1.Add(media::TimeInterval(media::Microseconds(16), media::Microseconds(27)));
+ i1.Add(media::TimeInterval(media::Microseconds(7), media::Microseconds(15)));
+ i1.Add(media::TimeInterval(media::Microseconds(53), media::Microseconds(57)));
+ i1.Add(media::TimeInterval(media::Microseconds(45), media::Microseconds(50)));
+
+ media::TimeIntervals i(i0 + i1);
+ RefPtr<dom::TimeRanges> tr = new dom::TimeRanges();
+ i.ToTimeRanges(tr);
+ 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{media::TimeIntervals::FromTimeRanges(tr)};
+ RefPtr<dom::TimeRanges> tr2 = new dom::TimeRanges();
+ i_oo.ToTimeRanges(tr2);
+ 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(mozilla::IsSame<nsTArray_CopyChooser<IntIntervals>::Type, nsTArray_CopyWithConstructors<IntIntervals>>::value, "Must use copy constructor");
+ static_assert(mozilla::IsSame<nsTArray_CopyChooser<media::TimeIntervals>::Type, nsTArray_CopyWithConstructors<media::TimeIntervals>>::value, "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_EQ(0u, i0.Length());
+
+ 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/TestMP3Demuxer.cpp b/dom/media/gtest/TestMP3Demuxer.cpp
new file mode 100644
index 0000000000..8d2109f008
--- /dev/null
+++ b/dom/media/gtest/TestMP3Demuxer.cpp
@@ -0,0 +1,454 @@
+/* -*- 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"
+
+using namespace mozilla;
+using namespace mozilla::mp3;
+using media::TimeUnit;
+
+
+// Regular MP3 file mock resource.
+class MockMP3MediaResource : public MockMediaResource {
+public:
+ explicit MockMP3MediaResource(const char* aFileName)
+ : MockMediaResource(aFileName)
+ {}
+
+protected:
+ virtual ~MockMP3MediaResource() {}
+};
+
+// MP3 stream mock resource.
+class MockMP3StreamMediaResource : public MockMP3MediaResource {
+public:
+ explicit MockMP3StreamMediaResource(const char* aFileName)
+ : MockMP3MediaResource(aFileName)
+ {}
+
+ int64_t GetLength() override { return -1; }
+
+protected:
+ virtual ~MockMP3StreamMediaResource() {}
+};
+
+struct MP3Resource {
+ const char* mFilePath;
+ bool mIsVBR;
+ int64_t mFileSize;
+ int32_t mMPEGLayer;
+ int32_t mMPEGVersion;
+ uint8_t mID3MajorVersion;
+ uint8_t mID3MinorVersion;
+ uint8_t mID3Flags;
+ uint32_t mID3Size;
+
+ int64_t mDuration;
+ float mDurationError;
+ 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.mFileSize = 965257;
+ res.mMPEGLayer = 3;
+ res.mMPEGVersion = 1;
+ res.mID3MajorVersion = 3;
+ res.mID3MinorVersion = 0;
+ res.mID3Flags = 0;
+ res.mID3Size = 2141;
+ res.mDuration = 30067000;
+ res.mDurationError = 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 = -1;
+ streamRes.mDurationError = 0.0f;
+
+ 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.mFileSize = 191302;
+ res.mMPEGLayer = 3;
+ res.mMPEGVersion = 1;
+ res.mID3MajorVersion = 3;
+ res.mID3MinorVersion = 0;
+ res.mID3Flags = 0;
+ res.mID3Size = 115304;
+ res.mDuration = 3160816;
+ res.mDurationError = 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 = -1;
+ streamRes.mDurationError = 0.0f;
+
+ 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.mFileSize = 583679;
+ res.mMPEGLayer = 3;
+ res.mMPEGVersion = 1;
+ res.mID3MajorVersion = 3;
+ res.mID3MinorVersion = 0;
+ res.mID3Flags = 0;
+ res.mID3Size = 2221;
+ res.mDuration = 30081000;
+ res.mDurationError = 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.mFileSize = 6825;
+ res.mMPEGLayer = 3;
+ res.mMPEGVersion = 1;
+ res.mID3MajorVersion = 4;
+ res.mID3MinorVersion = 0;
+ res.mID3Flags = 0;
+ res.mID3Size = 24;
+ res.mDuration = 336686;
+ res.mDurationError = 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.mFileSize = 6845;
+ res.mMPEGLayer = 3;
+ res.mMPEGVersion = 1;
+ res.mID3MajorVersion = 4;
+ res.mID3MinorVersion = 0;
+ res.mID3Flags = 0;
+ res.mID3Size = 24;
+ res.mDuration = 336686;
+ res.mDurationError = 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);
+ }
+
+ for (auto& target: mTargets) {
+ ASSERT_EQ(NS_OK, target.mResource->Open(nullptr));
+ 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.mIsVBR) {
+ 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 {
+ 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) {
+ EXPECT_NEAR(target.mDuration, target.mDemuxer->Duration().ToMicroseconds(),
+ target.mDurationError * target.mDuration);
+
+ 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);
+
+ const int64_t duration = target.mDemuxer->Duration().ToMicroseconds();
+ const int64_t pos = duration + 1e6;
+
+ // Attempt to seek 1 second past the end of stream.
+ target.mDemuxer->Seek(TimeUnit::FromMicroseconds(pos));
+ // The seek should bring us to the end of the stream.
+ EXPECT_NEAR(duration, target.mDemuxer->SeekPosition().ToMicroseconds(),
+ target.mSeekError * duration);
+
+ // 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 int64_t seekTime = TimeUnit::FromSeconds(1).ToMicroseconds();
+ int64_t pos = target.mDemuxer->SeekPosition().ToMicroseconds();
+
+ while (frameData) {
+ EXPECT_NEAR(pos, target.mDemuxer->SeekPosition().ToMicroseconds(),
+ target.mSeekError * pos);
+
+ pos += seekTime;
+ target.mDemuxer->Seek(TimeUnit::FromMicroseconds(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 int64_t seekTime = TimeUnit::FromSeconds(1).ToMicroseconds();
+ int64_t pos = target.mDemuxer->SeekPosition().ToMicroseconds();
+
+ while (frameData) {
+ EXPECT_NEAR(pos, target.mDemuxer->SeekPosition().ToMicroseconds(),
+ target.mSeekError * pos);
+
+ pos += seekTime;
+ target.mDemuxer->Reset();
+ target.mDemuxer->Seek(TimeUnit::FromMicroseconds(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..f1210fde77
--- /dev/null
+++ b/dom/media/gtest/TestMP4Demuxer.cpp
@@ -0,0 +1,463 @@
+/* -*- 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 "MP4Stream.h"
+#include "mozilla/MozPromise.h"
+#include "MediaDataDemuxer.h"
+#include "mozilla/SharedThreadPool.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/ArrayUtils.h"
+#include "MockMediaResource.h"
+#include "VideoUtils.h"
+
+using namespace mozilla;
+using namespace mp4_demuxer;
+
+class AutoTaskQueue;
+
+#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::PLAYBACK)))
+ , mIndex(0)
+ {
+ EXPECT_EQ(NS_OK, resource->Open(nullptr));
+ }
+
+ template<typename Function>
+ void RunTestAndWait(const Function& aFunction)
+ {
+ Function func(aFunction);
+ RefPtr<MP4DemuxerBinding> binding = this;
+ mDemuxer->Init()->Then(mTaskQueue, __func__, Move(func), DO_FAIL);
+ mTaskQueue->AwaitShutdownAndIdle();
+ }
+
+ RefPtr<GenericPromise>
+ CheckTrackKeyFrame(MediaTrackDemuxer* aTrackDemuxer)
+ {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+
+ RefPtr<MediaTrackDemuxer> track = aTrackDemuxer;
+ RefPtr<MP4DemuxerBinding> binding = this;
+
+ int64_t time = -1;
+ 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 == -1) {
+ mCheckTrackKeyFramePromise.Resolve(true, __func__);
+ return p;
+ }
+
+
+ DispatchTask(
+ [track, time, binding] () {
+ track->Seek(media::TimeUnit::FromMicroseconds(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->mSamples[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->mSamples.Length()) {
+ binding->mSamples.AppendElements(aSamples->mSamples);
+ 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);
+ }
+ }
+ 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(aFun);
+ mTaskQueue->Dispatch(r.forget());
+ }
+
+ virtual ~MP4DemuxerBinding()
+ {
+ }
+};
+
+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.mValid) {
+ res.AppendPrintf("%d %d ", aCrypto.mMode, aCrypto.mIVSize);
+ for (size_t i = 0; i < aCrypto.mKeyId.Length(); i++) {
+ res.AppendPrintf("%02x", aCrypto.mKeyId[i]);
+ }
+ res.Append(" ");
+ 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.Append("no crypto");
+ }
+ return res;
+}
+
+#ifndef XP_WIN // VC2013 doesn't support C++11 array initialization.
+
+TEST(MP4Demuxer, CENCFragVideo)
+{
+ const char* video[] = {
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000000 5,684 5,16980",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000450 5,1826",
+ "1 16 7e571d037e571d037e571d037e571d03 000000000000000000000000000004c3 5,1215",
+ "1 16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000050f 5,1302",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000561 5,939",
+ "1 16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000059c 5,763",
+ "1 16 7e571d037e571d037e571d037e571d03 000000000000000000000000000005cc 5,672",
+ "1 16 7e571d037e571d037e571d037e571d03 000000000000000000000000000005f6 5,748",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000625 5,1025",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000666 5,730",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000694 5,897",
+ "1 16 7e571d037e571d037e571d037e571d03 000000000000000000000000000006cd 5,643",
+ "1 16 7e571d037e571d037e571d037e571d03 000000000000000000000000000006f6 5,556",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000719 5,527",
+ "1 16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000073a 5,606",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000760 5,701",
+ "1 16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000078c 5,531",
+ "1 16 7e571d037e571d037e571d037e571d03 000000000000000000000000000007ae 5,562",
+ "1 16 7e571d037e571d037e571d037e571d03 000000000000000000000000000007d2 5,576",
+ "1 16 7e571d037e571d037e571d037e571d03 000000000000000000000000000007f6 5,514",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000817 5,404",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000831 5,635",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000859 5,433",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000875 5,478",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000893 5,474",
+ "1 16 7e571d037e571d037e571d037e571d03 000000000000000000000000000008b1 5,462",
+ "1 16 7e571d037e571d037e571d037e571d03 000000000000000000000000000008ce 5,473",
+ "1 16 7e571d037e571d037e571d037e571d03 000000000000000000000000000008ec 5,437",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000908 5,418",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000923 5,475",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000941 5,23133",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000ee7 5,475",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f05 5,402",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f1f 5,415",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f39 5,408",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f53 5,442",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f6f 5,385",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f88 5,368",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f9f 5,354",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000fb6 5,400",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000fcf 5,399",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000fe8 5,1098",
+ "1 16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000102d 5,1508",
+ "1 16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000108c 5,1345",
+ "1 16 7e571d037e571d037e571d037e571d03 000000000000000000000000000010e1 5,1945",
+ "1 16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000115b 5,1824",
+ "1 16 7e571d037e571d037e571d037e571d03 000000000000000000000000000011cd 5,2133",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000001253 5,2486",
+ "1 16 7e571d037e571d037e571d037e571d03 000000000000000000000000000012ef 5,1739",
+ "1 16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000135c 5,1836",
+ "1 16 7e571d037e571d037e571d037e571d03 000000000000000000000000000013cf 5,2367",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000001463 5,2571",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000001504 5,3008",
+ "1 16 7e571d037e571d037e571d037e571d03 000000000000000000000000000015c0 5,3255",
+ "1 16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000168c 5,3225",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000001756 5,3118",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000001819 5,2407",
+ "1 16 7e571d037e571d037e571d037e571d03 000000000000000000000000000018b0 5,2400",
+ "1 16 7e571d037e571d037e571d037e571d03 00000000000000000000000000001946 5,2158",
+ "1 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[] = {
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000000 0,281",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000012 0,257",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000023 0,246",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000033 0,257",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000044 0,260",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000055 0,260",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000066 0,272",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000077 0,280",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000089 0,284",
+ "1 16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000009b 0,290",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000000ae 0,278",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000000c0 0,268",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000000d1 0,307",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000000e5 0,290",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000000f8 0,304",
+ "1 16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000010b 0,316",
+ "1 16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000011f 0,308",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000133 0,301",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000146 0,318",
+ "1 16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000015a 0,311",
+ "1 16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000016e 0,303",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000181 0,325",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000196 0,334",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000001ab 0,344",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000001c1 0,344",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000001d7 0,387",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000001f0 0,396",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000209 0,368",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000220 0,373",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000238 0,425",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000253 0,428",
+ "1 16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000026e 0,426",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000289 0,427",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000002a4 0,424",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000002bf 0,447",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000002db 0,446",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000002f7 0,442",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000313 0,444",
+ "1 16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000032f 0,374",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000347 0,405",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000361 0,372",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000379 0,395",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000392 0,435",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000003ae 0,426",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000003c9 0,430",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000003e4 0,390",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000003fd 0,335",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000412 0,339",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000428 0,352",
+ "1 16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000043e 0,364",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000455 0,398",
+ "1 16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000046e 0,451",
+ "1 16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000048b 0,448",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000004a7 0,436",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000004c3 0,424",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000004de 0,428",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000004f9 0,413",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000513 0,430",
+ "1 16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000052e 0,450",
+ "1 16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000054b 0,386",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000564 0,320",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000578 0,347",
+ "1 16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000058e 0,382",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000005a6 0,437",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000005c2 0,387",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000005db 0,340",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000005f1 0,337",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000607 0,389",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000620 0,428",
+ "1 16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000063b 0,426",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000656 0,446",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000672 0,456",
+ "1 16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000068f 0,468",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000006ad 0,468",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000006cb 0,463",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000006e8 0,467",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000706 0,460",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000723 0,446",
+ "1 16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000073f 0,453",
+ "1 16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000075c 0,448",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000778 0,446",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000794 0,439",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000007b0 0,436",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000007cc 0,441",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000007e8 0,465",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000806 0,448",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000822 0,448",
+ "1 16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000083e 0,469",
+ "1 16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000085c 0,431",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000877 0,437",
+ "1 16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000893 0,474",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000008b1 0,436",
+ "1 16 7e571d047e571d047e571d047e571d04 000000000000000000000000000008cd 0,433",
+ "1 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);
+ });
+}
+
+#endif
+
+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.
+ media::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] () {
+ media::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/TestMP4Reader.cpp b/dom/media/gtest/TestMP4Reader.cpp
new file mode 100644
index 0000000000..f08f7a40da
--- /dev/null
+++ b/dom/media/gtest/TestMP4Reader.cpp
@@ -0,0 +1,217 @@
+/* -*- 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 "MP4Reader.h"
+#include "MP4Decoder.h"
+#include "mozilla/SharedThreadPool.h"
+#include "MockMediaResource.h"
+#include "MockMediaDecoderOwner.h"
+#include "mozilla/Preferences.h"
+#include "TimeUnits.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+class TestBinding
+{
+public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TestBinding);
+
+ RefPtr<MP4Decoder> decoder;
+ RefPtr<MockMediaResource> resource;
+ RefPtr<MP4Reader> reader;
+
+ explicit TestBinding(const char* aFileName = "gizmo.mp4")
+ : decoder(new MP4Decoder())
+ , resource(new MockMediaResource(aFileName))
+ , reader(new MP4Reader(decoder))
+ {
+ EXPECT_EQ(NS_OK, Preferences::SetBool(
+ "media.use-blank-decoder", true));
+
+ EXPECT_EQ(NS_OK, resource->Open(nullptr));
+ decoder->SetResource(resource);
+
+ reader->Init(nullptr);
+ // This needs to be done before invoking GetBuffered. This is normally
+ // done by MediaDecoderStateMachine.
+ reader->DispatchSetStartTime(0);
+ }
+
+ void Init() {
+ nsCOMPtr<nsIThread> thread;
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(this, &TestBinding::ReadMetadata);
+ nsresult rv = NS_NewThread(getter_AddRefs(thread), r);
+ EXPECT_EQ(NS_OK, rv);
+ thread->Shutdown();
+ }
+
+private:
+ virtual ~TestBinding()
+ {
+ {
+ RefPtr<TaskQueue> queue = reader->OwnerThread();
+ nsCOMPtr<nsIRunnable> task = NewRunnableMethod(reader, &MP4Reader::Shutdown);
+ // Hackily bypass the tail dispatcher so that we can AwaitShutdownAndIdle.
+ // In production code we'd use BeginShutdown + promises.
+ queue->Dispatch(task.forget(), AbstractThread::AssertDispatchSuccess,
+ AbstractThread::TailDispatch);
+ queue->AwaitShutdownAndIdle();
+ }
+ decoder = nullptr;
+ resource = nullptr;
+ reader = nullptr;
+ SharedThreadPool::SpinUntilEmpty();
+ }
+
+ void ReadMetadata()
+ {
+ MediaInfo info;
+ MetadataTags* tags;
+ EXPECT_EQ(NS_OK, reader->ReadMetadata(&info, &tags));
+ }
+};
+
+TEST(MP4Reader, BufferedRange)
+{
+ RefPtr<TestBinding> b = new TestBinding();
+ b->Init();
+
+ // Video 3-4 sec, audio 2.986666-4.010666 sec
+ b->resource->MockAddBufferedRange(248400, 327455);
+
+ media::TimeIntervals ranges = b->reader->GetBuffered();
+ EXPECT_EQ(1U, ranges.Length());
+ EXPECT_NEAR(270000 / 90000.0, ranges.Start(0).ToSeconds(), 0.000001);
+ EXPECT_NEAR(360000 / 90000.0, ranges.End(0).ToSeconds(), 0.000001);
+}
+
+TEST(MP4Reader, BufferedRangeMissingLastByte)
+{
+ RefPtr<TestBinding> b = new TestBinding();
+ b->Init();
+
+ // Dropping the last byte of the video
+ b->resource->MockClearBufferedRanges();
+ b->resource->MockAddBufferedRange(248400, 324912);
+ b->resource->MockAddBufferedRange(324913, 327455);
+
+ media::TimeIntervals ranges = b->reader->GetBuffered();
+ EXPECT_EQ(1U, ranges.Length());
+ EXPECT_NEAR(270000.0 / 90000.0, ranges.Start(0).ToSeconds(), 0.000001);
+ EXPECT_NEAR(357000 / 90000.0, ranges.End(0).ToSeconds(), 0.000001);
+}
+
+TEST(MP4Reader, BufferedRangeSyncFrame)
+{
+ RefPtr<TestBinding> b = new TestBinding();
+ b->Init();
+
+ // Check that missing the first byte at 2 seconds skips right through to 3
+ // seconds because of a missing sync frame
+ b->resource->MockClearBufferedRanges();
+ b->resource->MockAddBufferedRange(146336, 327455);
+
+ media::TimeIntervals ranges = b->reader->GetBuffered();
+ EXPECT_EQ(1U, ranges.Length());
+ EXPECT_NEAR(270000.0 / 90000.0, ranges.Start(0).ToSeconds(), 0.000001);
+ EXPECT_NEAR(360000 / 90000.0, ranges.End(0).ToSeconds(), 0.000001);
+}
+
+TEST(MP4Reader, CompositionOrder)
+{
+ RefPtr<TestBinding> b = new TestBinding("mediasource_test.mp4");
+ b->Init();
+
+ // The first 5 video samples of this file are:
+ // Video timescale=2500
+ // Frame Start Size Time Duration Sync
+ // 1 48 5455 166 83 Yes
+ // 2 5503 145 249 83
+ // 3 6228 575 581 83
+ // 4 7383 235 415 83
+ // 5 8779 183 332 83
+ // 6 9543 191 498 83
+ //
+ // Audio timescale=44100
+ // 1 5648 580 0 1024 Yes
+ // 2 6803 580 1024 1058 Yes
+ // 3 7618 581 2082 1014 Yes
+ // 4 8199 580 3096 1015 Yes
+ // 5 8962 581 4111 1014 Yes
+ // 6 9734 580 5125 1014 Yes
+ // 7 10314 581 6139 1059 Yes
+ // 8 11207 580 7198 1014 Yes
+ // 9 12035 581 8212 1014 Yes
+ // 10 12616 580 9226 1015 Yes
+ // 11 13220 581 10241 1014 Yes
+
+ b->resource->MockClearBufferedRanges();
+ // First two frames in decoding + first audio frame
+ b->resource->MockAddBufferedRange(48, 5503); // Video 1
+ b->resource->MockAddBufferedRange(5503, 5648); // Video 2
+ b->resource->MockAddBufferedRange(6228, 6803); // Video 3
+
+ // Audio - 5 frames; 0 - 139206 us
+ b->resource->MockAddBufferedRange(5648, 6228);
+ b->resource->MockAddBufferedRange(6803, 7383);
+ b->resource->MockAddBufferedRange(7618, 8199);
+ b->resource->MockAddBufferedRange(8199, 8779);
+ b->resource->MockAddBufferedRange(8962, 9563);
+ b->resource->MockAddBufferedRange(9734, 10314);
+ b->resource->MockAddBufferedRange(10314, 10895);
+ b->resource->MockAddBufferedRange(11207, 11787);
+ b->resource->MockAddBufferedRange(12035, 12616);
+ b->resource->MockAddBufferedRange(12616, 13196);
+ b->resource->MockAddBufferedRange(13220, 13901);
+
+ media::TimeIntervals ranges = b->reader->GetBuffered();
+ EXPECT_EQ(2U, ranges.Length());
+
+ EXPECT_NEAR(166.0 / 2500.0, ranges.Start(0).ToSeconds(), 0.000001);
+ EXPECT_NEAR(332.0 / 2500.0, ranges.End(0).ToSeconds(), 0.000001);
+
+ EXPECT_NEAR(581.0 / 2500.0, ranges.Start(1).ToSeconds(), 0.000001);
+ EXPECT_NEAR(11255.0 / 44100.0, ranges.End(1).ToSeconds(), 0.000001);
+}
+
+TEST(MP4Reader, Normalised)
+{
+ RefPtr<TestBinding> b = new TestBinding("mediasource_test.mp4");
+ b->Init();
+
+ // The first 5 video samples of this file are:
+ // Video timescale=2500
+ // Frame Start Size Time Duration Sync
+ // 1 48 5455 166 83 Yes
+ // 2 5503 145 249 83
+ // 3 6228 575 581 83
+ // 4 7383 235 415 83
+ // 5 8779 183 332 83
+ // 6 9543 191 498 83
+ //
+ // Audio timescale=44100
+ // 1 5648 580 0 1024 Yes
+ // 2 6803 580 1024 1058 Yes
+ // 3 7618 581 2082 1014 Yes
+ // 4 8199 580 3096 1015 Yes
+ // 5 8962 581 4111 1014 Yes
+ // 6 9734 580 5125 1014 Yes
+ // 7 10314 581 6139 1059 Yes
+ // 8 11207 580 7198 1014 Yes
+ // 9 12035 581 8212 1014 Yes
+ // 10 12616 580 9226 1015 Yes
+ // 11 13220 581 10241 1014 Yes
+
+ b->resource->MockClearBufferedRanges();
+ b->resource->MockAddBufferedRange(48, 13901);
+
+ media::TimeIntervals ranges = b->reader->GetBuffered();
+ EXPECT_EQ(1U, ranges.Length());
+
+ EXPECT_NEAR(166.0 / 2500.0, ranges.Start(0).ToSeconds(), 0.000001);
+ EXPECT_NEAR(11255.0 / 44100.0, ranges.End(0).ToSeconds(), 0.000001);
+}
diff --git a/dom/media/gtest/TestMediaDataDecoder.cpp b/dom/media/gtest/TestMediaDataDecoder.cpp
new file mode 100644
index 0000000000..47e78bf7ed
--- /dev/null
+++ b/dom/media/gtest/TestMediaDataDecoder.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 "Benchmark.h"
+#include "MockMediaResource.h"
+#include "DecoderTraits.h"
+#include "MP4Demuxer.h"
+#include "WebMDemuxer.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(
+ AbstractThread::MainThread(), __func__,
+ [&](uint32_t aDecodeFps) { result = aDecodeFps; done = true; },
+ [&]() { done = true; });
+
+ // Wait until benchmark completes.
+ while (!done) {
+ NS_ProcessNextEvent();
+ }
+ return result;
+ }
+
+private:
+ RefPtr<Benchmark> mBenchmark;
+};
+
+TEST(MediaDataDecoder, H264)
+{
+ if (!DecoderTraits::IsMP4TypeAndEnabled(NS_LITERAL_CSTRING("video/mp4")
+ , /* DecoderDoctorDiagnostics* */ nullptr)) {
+ EXPECT_TRUE(true);
+ } else {
+ RefPtr<MediaResource> resource =
+ new MockMediaResource("gizmo.mp4", NS_LITERAL_CSTRING("video/mp4"));
+ nsresult rv = resource->Open(nullptr);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+ BenchmarkRunner runner(new Benchmark(new MP4Demuxer(resource)));
+ EXPECT_GT(runner.Run(), 0u);
+ }
+}
+
+TEST(MediaDataDecoder, VP9)
+{
+ if (!DecoderTraits::IsWebMTypeAndEnabled(NS_LITERAL_CSTRING("video/webm"))) {
+ EXPECT_TRUE(true);
+ } else {
+ RefPtr<MediaResource> resource =
+ new MockMediaResource("vp9cake.webm", NS_LITERAL_CSTRING("video/webm"));
+ nsresult rv = resource->Open(nullptr);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+ BenchmarkRunner runner(new Benchmark(new WebMDemuxer(resource)));
+ EXPECT_GT(runner.Run(), 0u);
+ }
+}
diff --git a/dom/media/gtest/TestMediaEventSource.cpp b/dom/media/gtest/TestMediaEventSource.cpp
new file mode 100644
index 0000000000..75fb09f201
--- /dev/null
+++ b/dom/media/gtest/TestMediaEventSource.cpp
@@ -0,0 +1,337 @@
+/* -*- 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/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::PLAYBACK));
+
+ 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::PLAYBACK));
+
+ 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::PLAYBACK));
+
+ 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::PLAYBACK));
+
+ 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::PLAYBACK));
+
+ 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::PLAYBACK));
+
+ 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::PLAYBACK));
+
+ 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;
+ }
+ int& mCount;
+};
+
+/*
+ * Test we don't have unnecessary copies of the event data.
+ */
+TEST(MediaEventSource, CopyEvent1)
+{
+ RefPtr<TaskQueue> queue = new TaskQueue(
+ GetMediaThreadPool(MediaThreadType::PLAYBACK));
+
+ 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::PLAYBACK));
+
+ 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::PLAYBACK));
+
+ 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();
+}
diff --git a/dom/media/gtest/TestMediaFormatReader.cpp b/dom/media/gtest/TestMediaFormatReader.cpp
new file mode 100644
index 0000000000..ad222e8b80
--- /dev/null
+++ b/dom/media/gtest/TestMediaFormatReader.cpp
@@ -0,0 +1,248 @@
+/* -*- 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/dom/HTMLMediaElement.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/TaskQueue.h"
+#include "ImageContainer.h"
+#include "Layers.h"
+#include "MediaData.h"
+#include "MediaFormatReader.h"
+#include "MP4Decoder.h"
+#include "MockMediaDecoderOwner.h"
+#include "MockMediaResource.h"
+#include "VideoFrameContainer.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+class MockMP4Decoder : public MP4Decoder
+{
+public:
+ MockMP4Decoder()
+ : MP4Decoder(new MockMediaDecoderOwner())
+ {}
+
+ // Avoid the assertion.
+ AbstractCanonical<media::NullableTimeUnit>* CanonicalDurationOrNull() override
+ {
+ return nullptr;
+ }
+};
+
+class MediaFormatReaderBinding
+{
+public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaFormatReaderBinding);
+ RefPtr<MockMP4Decoder> mDecoder;
+ RefPtr<MockMediaResource> mResource;
+ RefPtr<MediaDecoderReader> mReader;
+ RefPtr<TaskQueue> mTaskQueue;
+ explicit MediaFormatReaderBinding(const char* aFileName = "gizmo.mp4")
+ : mDecoder(new MockMP4Decoder())
+ , mResource(new MockMediaResource(aFileName))
+ , mReader(new MediaFormatReader(mDecoder,
+ new MP4Demuxer(mResource),
+ new VideoFrameContainer(
+ (HTMLMediaElement*)(0x1),
+ layers::LayerManager::CreateImageContainer(
+ layers::ImageContainer::ASYNCHRONOUS))
+ ))
+ , mTaskQueue(new TaskQueue(GetMediaThreadPool(MediaThreadType::PLAYBACK)))
+ {
+ }
+
+ bool Init() {
+ // Init Resource.
+ nsresult rv = mResource->Open(nullptr);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ mDecoder->SetResource(mResource);
+ // Init Reader.
+ rv = mReader->Init();
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ return true;
+ }
+
+ void OnMetadataReadAudio(MetadataHolder* aMetadata)
+ {
+ EXPECT_TRUE(aMetadata);
+ mReader->RequestAudioData()
+ ->Then(mReader->OwnerThread(), __func__, this,
+ &MediaFormatReaderBinding::OnAudioRawDataDemuxed,
+ &MediaFormatReaderBinding::OnNotDemuxed);
+ }
+
+ void OnMetadataReadVideo(MetadataHolder* aMetadata)
+ {
+ EXPECT_TRUE(aMetadata);
+ mReader->RequestVideoData(true, 0)
+ ->Then(mReader->OwnerThread(), __func__, this,
+ &MediaFormatReaderBinding::OnVideoRawDataDemuxed,
+ &MediaFormatReaderBinding::OnNotDemuxed);
+ }
+
+ void OnMetadataNotRead(const MediaResult& aError) {
+ EXPECT_TRUE(false);
+ ReaderShutdown();
+ }
+
+ void OnAudioRawDataDemuxed(MediaData* aAudioSample)
+ {
+ EXPECT_TRUE(aAudioSample);
+ EXPECT_EQ(MediaData::RAW_DATA, aAudioSample->mType);
+ ReaderShutdown();
+ }
+
+ void OnVideoRawDataDemuxed(MediaData* aVideoSample)
+ {
+ EXPECT_TRUE(aVideoSample);
+ EXPECT_EQ(MediaData::RAW_DATA, aVideoSample->mType);
+ ReaderShutdown();
+ }
+
+ void OnNotDemuxed(const MediaResult& aReason)
+ {
+ EXPECT_TRUE(false);
+ ReaderShutdown();
+ }
+
+ void ReaderShutdown()
+ {
+ RefPtr<MediaFormatReaderBinding> self = this;
+ mReader->Shutdown()
+ ->Then(mTaskQueue, __func__,
+ [self]() {
+ self->mTaskQueue->BeginShutdown();
+ },
+ [self]() {
+ EXPECT_TRUE(false);
+ self->mTaskQueue->BeginShutdown();
+ }); //Then
+ }
+ template<class Function>
+ void RunTestAndWait(Function&& aFunction)
+ {
+ RefPtr<Runnable> r = NS_NewRunnableFunction(Forward<Function>(aFunction));
+ mTaskQueue->Dispatch(r.forget());
+ mTaskQueue->AwaitShutdownAndIdle();
+ }
+private:
+ ~MediaFormatReaderBinding()
+ {
+ mDecoder->Shutdown();
+ }
+};
+
+
+template <typename T>
+T GetPref(const char* aPrefKey);
+
+template <>
+bool GetPref<bool>(const char* aPrefKey)
+{
+ return Preferences::GetBool(aPrefKey);
+}
+
+template <typename T>
+void SetPref(const char* a, T value);
+
+template <>
+void SetPref<bool>(const char* aPrefKey, bool aValue)
+{
+ Unused << Preferences::SetBool(aPrefKey, aValue);
+}
+
+template <typename T>
+class PreferencesRAII
+{
+public:
+ explicit PreferencesRAII(const char* aPrefKey, T aValue)
+ : mPrefKey(aPrefKey)
+ {
+ mDefaultPref = GetPref<T>(aPrefKey);
+ SetPref(aPrefKey, aValue);
+ }
+ ~PreferencesRAII()
+ {
+ SetPref(mPrefKey, mDefaultPref);
+ }
+private:
+ T mDefaultPref;
+ const char* mPrefKey;
+};
+
+TEST(MediaFormatReader, RequestAudioRawData)
+{
+ PreferencesRAII<bool> pref =
+ PreferencesRAII<bool>("media.use-blank-decoder",
+ true);
+ RefPtr<MediaFormatReaderBinding> b = new MediaFormatReaderBinding();
+ if (!b->Init())
+ {
+ EXPECT_TRUE(false);
+ // Stop the test since initialization failed.
+ return;
+ }
+ if (!b->mReader->IsDemuxOnlySupported())
+ {
+ EXPECT_TRUE(false);
+ // Stop the test since the reader cannot support demuxed-only demand.
+ return;
+ }
+ // Switch to demuxed-only mode.
+ b->mReader->SetDemuxOnly(true);
+ // To ensure the MediaDecoderReader::InitializationTask and
+ // MediaDecoderReader::SetDemuxOnly can be done.
+ NS_ProcessNextEvent();
+ auto testCase = [b]() {
+ InvokeAsync(b->mReader->OwnerThread(),
+ b->mReader.get(),
+ __func__,
+ &MediaDecoderReader::AsyncReadMetadata)
+ ->Then(b->mReader->OwnerThread(), __func__, b.get(),
+ &MediaFormatReaderBinding::OnMetadataReadAudio,
+ &MediaFormatReaderBinding::OnMetadataNotRead);
+ };
+ b->RunTestAndWait(testCase);
+}
+TEST(MediaFormatReader, RequestVideoRawData)
+{
+ PreferencesRAII<bool> pref =
+ PreferencesRAII<bool>("media.use-blank-decoder",
+ true);
+ RefPtr<MediaFormatReaderBinding> b = new MediaFormatReaderBinding();
+ if (!b->Init())
+ {
+ EXPECT_TRUE(false);
+ // Stop the test since initialization failed.
+ return;
+ }
+ if (!b->mReader->IsDemuxOnlySupported())
+ {
+ EXPECT_TRUE(false);
+ // Stop the test since the reader cannot support demuxed-only demand.
+ return;
+ }
+ // Switch to demuxed-only mode.
+ b->mReader->SetDemuxOnly(true);
+ // To ensure the MediaDecoderReader::InitializationTask and
+ // MediaDecoderReader::SetDemuxOnly can be done.
+ NS_ProcessNextEvent();
+ auto testCase = [b]() {
+ InvokeAsync(b->mReader->OwnerThread(),
+ b->mReader.get(),
+ __func__,
+ &MediaDecoderReader::AsyncReadMetadata)
+ ->Then(b->mReader->OwnerThread(), __func__, b.get(),
+ &MediaFormatReaderBinding::OnMetadataReadVideo,
+ &MediaFormatReaderBinding::OnMetadataNotRead);
+ };
+ b->RunTestAndWait(testCase);
+}
diff --git a/dom/media/gtest/TestMozPromise.cpp b/dom/media/gtest/TestMozPromise.cpp
new file mode 100644
index 0000000000..b1d363bd54
--- /dev/null
+++ b/dom/media/gtest/TestMozPromise.cpp
@@ -0,0 +1,257 @@
+/* -*- 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/TaskQueue.h"
+#include "mozilla/MozPromise.h"
+
+#include "nsISupportsImpl.h"
+#include "mozilla/SharedThreadPool.h"
+#include "VideoUtils.h"
+
+using namespace mozilla;
+
+typedef MozPromise<int, double, false> TestPromise;
+typedef TestPromise::ResolveOrRejectValue RRValue;
+
+class MOZ_STACK_CLASS AutoTaskQueue
+{
+public:
+ AutoTaskQueue()
+ : mTaskQueue(new TaskQueue(GetMediaThreadPool(MediaThreadType::PLAYBACK)))
+ {}
+
+ ~AutoTaskQueue()
+ {
+ mTaskQueue->AwaitShutdownAndIdle();
+ }
+
+ TaskQueue* Queue() { return mTaskQueue; }
+private:
+ RefPtr<TaskQueue> mTaskQueue;
+};
+
+class DelayedResolveOrReject : public Runnable
+{
+public:
+ DelayedResolveOrReject(TaskQueue* aTaskQueue,
+ TestPromise::Private* aPromise,
+ TestPromise::ResolveOrRejectValue aValue,
+ int aIterations)
+ : mTaskQueue(aTaskQueue)
+ , mPromise(aPromise)
+ , mValue(aValue)
+ , mIterations(aIterations)
+ {}
+
+ NS_IMETHOD Run() override
+ {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+ if (!mPromise) {
+ // Canceled.
+ return NS_OK;
+ }
+
+ if (--mIterations == 0) {
+ mPromise->ResolveOrReject(mValue, __func__);
+ } else {
+ nsCOMPtr<nsIRunnable> r = this;
+ mTaskQueue->Dispatch(r.forget());
+ }
+
+ return NS_OK;
+ }
+
+ void Cancel() {
+ mPromise = nullptr;
+ }
+
+protected:
+ ~DelayedResolveOrReject() {}
+
+private:
+ RefPtr<TaskQueue> mTaskQueue;
+ RefPtr<TestPromise::Private> mPromise;
+ TestPromise::ResolveOrRejectValue mValue;
+ int mIterations;
+};
+
+template<typename FunctionType>
+void
+RunOnTaskQueue(TaskQueue* aQueue, FunctionType aFun)
+{
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(aFun);
+ aQueue->Dispatch(r.forget());
+}
+
+// std::function can't come soon enough. :-(
+#define DO_FAIL []()->void { EXPECT_TRUE(false); }
+
+TEST(MozPromise, BasicResolve)
+{
+ AutoTaskQueue atq;
+ RefPtr<TaskQueue> queue = atq.Queue();
+ RunOnTaskQueue(queue, [queue] () -> void {
+ TestPromise::CreateAndResolve(42, __func__)->Then(queue, __func__,
+ [queue] (int aResolveValue) -> void { EXPECT_EQ(aResolveValue, 42); queue->BeginShutdown(); },
+ DO_FAIL);
+ });
+}
+
+TEST(MozPromise, BasicReject)
+{
+ AutoTaskQueue atq;
+ RefPtr<TaskQueue> queue = atq.Queue();
+ RunOnTaskQueue(queue, [queue] () -> void {
+ TestPromise::CreateAndReject(42.0, __func__)->Then(queue, __func__,
+ DO_FAIL,
+ [queue] (int aRejectValue) -> void { EXPECT_EQ(aRejectValue, 42.0); queue->BeginShutdown(); });
+ });
+}
+
+TEST(MozPromise, AsyncResolve)
+{
+ AutoTaskQueue atq;
+ RefPtr<TaskQueue> queue = atq.Queue();
+ RunOnTaskQueue(queue, [queue] () -> void {
+ RefPtr<TestPromise::Private> p = new TestPromise::Private(__func__);
+
+ // Kick off three racing tasks, and make sure we get the one that finishes earliest.
+ RefPtr<DelayedResolveOrReject> a = new DelayedResolveOrReject(queue, p, RRValue::MakeResolve(32), 10);
+ RefPtr<DelayedResolveOrReject> b = new DelayedResolveOrReject(queue, p, RRValue::MakeResolve(42), 5);
+ RefPtr<DelayedResolveOrReject> c = new DelayedResolveOrReject(queue, p, RRValue::MakeReject(32.0), 7);
+
+ nsCOMPtr<nsIRunnable> ref = a.get();
+ queue->Dispatch(ref.forget());
+ ref = b.get();
+ queue->Dispatch(ref.forget());
+ ref = c.get();
+ queue->Dispatch(ref.forget());
+
+ p->Then(queue, __func__, [queue, a, b, c] (int aResolveValue) -> void {
+ EXPECT_EQ(aResolveValue, 42);
+ a->Cancel();
+ b->Cancel();
+ c->Cancel();
+ queue->BeginShutdown();
+ }, DO_FAIL);
+ });
+}
+
+TEST(MozPromise, CompletionPromises)
+{
+ bool invokedPass = false;
+ AutoTaskQueue atq;
+ RefPtr<TaskQueue> queue = atq.Queue();
+ RunOnTaskQueue(queue, [queue, &invokedPass] () -> void {
+ TestPromise::CreateAndResolve(40, __func__)
+ ->Then(queue, __func__,
+ [] (int aVal) -> RefPtr<TestPromise> { return TestPromise::CreateAndResolve(aVal + 10, __func__); },
+ DO_FAIL)
+ ->CompletionPromise()
+ ->Then(queue, __func__, [&invokedPass] () -> void { invokedPass = true; }, DO_FAIL)
+ ->CompletionPromise()
+ ->Then(queue, __func__,
+ [queue] (int aVal) -> RefPtr<TestPromise> {
+ RefPtr<TestPromise::Private> p = new TestPromise::Private(__func__);
+ nsCOMPtr<nsIRunnable> resolver = new DelayedResolveOrReject(queue, p, RRValue::MakeResolve(aVal - 8), 10);
+ queue->Dispatch(resolver.forget());
+ return RefPtr<TestPromise>(p);
+ },
+ DO_FAIL)
+ ->CompletionPromise()
+ ->Then(queue, __func__,
+ [queue] (int aVal) -> RefPtr<TestPromise> { return TestPromise::CreateAndReject(double(aVal - 42) + 42.0, __func__); },
+ DO_FAIL)
+ ->CompletionPromise()
+ ->Then(queue, __func__,
+ DO_FAIL,
+ [queue, &invokedPass] (double aVal) -> void { EXPECT_EQ(aVal, 42.0); EXPECT_TRUE(invokedPass); queue->BeginShutdown(); });
+ });
+}
+
+TEST(MozPromise, PromiseAllResolve)
+{
+ AutoTaskQueue atq;
+ RefPtr<TaskQueue> queue = atq.Queue();
+ RunOnTaskQueue(queue, [queue] () -> void {
+
+ nsTArray<RefPtr<TestPromise>> promises;
+ promises.AppendElement(TestPromise::CreateAndResolve(22, __func__));
+ promises.AppendElement(TestPromise::CreateAndResolve(32, __func__));
+ promises.AppendElement(TestPromise::CreateAndResolve(42, __func__));
+
+ TestPromise::All(queue, promises)->Then(queue, __func__,
+ [queue] (const nsTArray<int>& aResolveValues) -> void {
+ EXPECT_EQ(aResolveValues.Length(), 3UL);
+ EXPECT_EQ(aResolveValues[0], 22);
+ EXPECT_EQ(aResolveValues[1], 32);
+ EXPECT_EQ(aResolveValues[2], 42);
+ queue->BeginShutdown();
+ },
+ DO_FAIL
+ );
+ });
+}
+
+TEST(MozPromise, PromiseAllReject)
+{
+ AutoTaskQueue atq;
+ RefPtr<TaskQueue> queue = atq.Queue();
+ RunOnTaskQueue(queue, [queue] () -> void {
+
+ nsTArray<RefPtr<TestPromise>> promises;
+ promises.AppendElement(TestPromise::CreateAndResolve(22, __func__));
+ promises.AppendElement(TestPromise::CreateAndReject(32.0, __func__));
+ promises.AppendElement(TestPromise::CreateAndResolve(42, __func__));
+ // Ensure that more than one rejection doesn't cause a crash (bug #1207312)
+ promises.AppendElement(TestPromise::CreateAndReject(52.0, __func__));
+
+ TestPromise::All(queue, promises)->Then(queue, __func__,
+ DO_FAIL,
+ [queue] (float aRejectValue) -> void {
+ EXPECT_EQ(aRejectValue, 32.0);
+ queue->BeginShutdown();
+ }
+ );
+ });
+}
+
+// Test we don't hit the assertions in MozPromise when exercising promise
+// chaining upon task queue shutdown.
+TEST(MozPromise, Chaining)
+{
+ AutoTaskQueue atq;
+ RefPtr<TaskQueue> queue = atq.Queue();
+ MozPromiseRequestHolder<TestPromise> holder;
+
+ RunOnTaskQueue(queue, [queue, &holder] () {
+ auto p = TestPromise::CreateAndResolve(42, __func__);
+ const size_t kIterations = 100;
+ for (size_t i = 0; i < kIterations; ++i) {
+ p = p->Then(queue, __func__,
+ [] (int aVal) {
+ EXPECT_EQ(aVal, 42);
+ },
+ [] () {}
+ )->CompletionPromise();
+
+ if (i == kIterations / 2) {
+ p->Then(queue, __func__,
+ [queue, &holder] () {
+ holder.Disconnect();
+ queue->BeginShutdown();
+ },
+ DO_FAIL);
+ }
+ }
+ // We will hit the assertion if we don't disconnect the leaf Request
+ // in the promise chain.
+ holder.Begin(p->Then(queue, __func__, [] () {}, [] () {}));
+ });
+}
+
+#undef DO_FAIL
diff --git a/dom/media/gtest/TestRust.cpp b/dom/media/gtest/TestRust.cpp
new file mode 100644
index 0000000000..86d0e99b86
--- /dev/null
+++ b/dom/media/gtest/TestRust.cpp
@@ -0,0 +1,9 @@
+#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..605e0e90c9
--- /dev/null
+++ b/dom/media/gtest/TestTimeUnit.cpp
@@ -0,0 +1,22 @@
+/* -*- 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;
+
+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);
+}
diff --git a/dom/media/gtest/TestTrackEncoder.cpp b/dom/media/gtest/TestTrackEncoder.cpp
new file mode 100644
index 0000000000..01d5c86e93
--- /dev/null
+++ b/dom/media/gtest/TestTrackEncoder.cpp
@@ -0,0 +1,94 @@
+/* -*- 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"
+
+using namespace mozilla;
+
+class TestOpusTrackEncoder : public OpusTrackEncoder
+{
+public:
+ // Return true if it has successfully initialized the Opus encoder.
+ bool TestOpusCreation(int aChannels, int aSamplingRate)
+ {
+ if (Init(aChannels, aSamplingRate) == NS_OK) {
+ if (GetPacketDuration()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Return the sample rate of data to be fed to the Opus encoder, could be
+ // re-sampled if it was not one of the Opus supported sampling rates.
+ // Init() is expected to be called first.
+ int TestGetOutputSampleRate()
+ {
+ return mInitialized ? GetOutputSampleRate() : 0;
+ }
+};
+
+static bool
+TestOpusInit(int aChannels, int aSamplingRate)
+{
+ TestOpusTrackEncoder encoder;
+ return encoder.TestOpusCreation(aChannels, aSamplingRate);
+}
+
+static int
+TestOpusResampler(int aChannels, int aSamplingRate)
+{
+ TestOpusTrackEncoder encoder;
+ EXPECT_TRUE(encoder.TestOpusCreation(aChannels, aSamplingRate));
+ return encoder.TestGetOutputSampleRate();
+}
+
+TEST(Media, OpusEncoder_Init)
+{
+ // 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(Media, OpusEncoder_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(1, 8000) == 8000);
+ EXPECT_TRUE(TestOpusResampler(1, 12000) == 12000);
+ EXPECT_TRUE(TestOpusResampler(1, 16000) == 16000);
+ EXPECT_TRUE(TestOpusResampler(1, 24000) == 24000);
+ EXPECT_TRUE(TestOpusResampler(1, 48000) == 48000);
+
+ // Otherwise, it should be resampled to 48kHz by resampler.
+ EXPECT_FALSE(TestOpusResampler(1, 9600) == 9600);
+ EXPECT_FALSE(TestOpusResampler(1, 44100) == 44100);
+}
diff --git a/dom/media/gtest/TestVPXDecoding.cpp b/dom/media/gtest/TestVPXDecoding.cpp
new file mode 100644
index 0000000000..d14c4d24e9
--- /dev/null
+++ b/dom/media/gtest/TestVPXDecoding.cpp
@@ -0,0 +1,103 @@
+/* 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/TestVideoSegment.cpp b/dom/media/gtest/TestVideoSegment.cpp
new file mode 100644
index 0000000000..f7d5aed213
--- /dev/null
+++ b/dom/media/gtest/TestVideoSegment.cpp
@@ -0,0 +1,51 @@
+/* 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 {
+ namespace layer {
+ class Image;
+ } // namespace layer
+} // namespace mozilla
+
+TEST(VideoSegment, TestAppendFrameForceBlack)
+{
+ RefPtr<layers::Image> testImage = nullptr;
+
+ VideoSegment segment;
+ segment.AppendFrame(testImage.forget(),
+ mozilla::StreamTime(90000),
+ 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::StreamTime(90000),
+ 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..65826c9b4d
--- /dev/null
+++ b/dom/media/gtest/TestVideoTrackEncoder.cpp
@@ -0,0 +1,351 @@
+/* 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 <algorithm>
+
+#include "mozilla/ArrayUtils.h"
+#include "VP8TrackEncoder.h"
+#include "ImageContainer.h"
+#include "MediaStreamGraph.h"
+#include "MediaStreamListener.h"
+#include "WebMWriter.h" // TODO: it's weird to include muxer header to get the class definition of VP8 METADATA
+
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+using namespace mozilla::layers;
+using namespace mozilla;
+
+// A helper object to generate of different YUV planes.
+class YUVBufferGenerator {
+public:
+ YUVBufferGenerator() {}
+
+ void 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 GetSize() const
+ {
+ return mImageSize;
+ }
+
+ void Generate(nsTArray<RefPtr<Image> > &aImages)
+ {
+ aImages.AppendElement(CreateI420Image());
+ aImages.AppendElement(CreateNV12Image());
+ aImages.AppendElement(CreateNV21Image());
+ }
+
+private:
+ Image *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;
+
+ image->CopyData(data);
+ return image;
+ }
+
+ Image *CreateNV12Image()
+ {
+ 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;
+
+ // 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->CopyData(data);
+ return image;
+ }
+
+ Image *CreateNV21Image()
+ {
+ 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;
+
+ // 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;
+
+ image->CopyData(data);
+ return image;
+ }
+
+private:
+ mozilla::gfx::IntSize mImageSize;
+ nsTArray<uint8_t> mSourceBuffer;
+};
+
+struct InitParam {
+ bool mShouldSucceed; // This parameter should cause success or fail result
+ int mWidth; // frame width
+ int mHeight; // frame height
+};
+
+class TestVP8TrackEncoder: public VP8TrackEncoder
+{
+public:
+ explicit TestVP8TrackEncoder(TrackRate aTrackRate = 90000)
+ : VP8TrackEncoder(aTrackRate) {}
+
+ ::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;
+ }
+ else
+ {
+ 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 (size_t i = 0; i < ArrayLength(params); i++)
+ {
+ TestVP8TrackEncoder encoder;
+ EXPECT_TRUE(encoder.TestInit(params[i]));
+ }
+}
+
+// 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 (size_t i = 0; i < ArrayLength(params); i++)
+ {
+ TestVP8TrackEncoder encoder;
+ EXPECT_TRUE(encoder.TestInit(params[i]));
+
+ RefPtr<TrackMetadataBase> meta = encoder.GetMetadata();
+ RefPtr<VP8Metadata> vp8Meta(static_cast<VP8Metadata*>(meta.get()));
+
+ // METADATA should be depend on how to initiate encoder.
+ EXPECT_TRUE(vp8Meta->mWidth == params[i].mWidth);
+ EXPECT_TRUE(vp8Meta->mHeight == params[i].mHeight);
+ }
+}
+
+// Encode test
+TEST(VP8VideoTrackEncoder, FrameEncode)
+{
+ // Initiate VP8 encoder
+ TestVP8TrackEncoder encoder;
+ InitParam param = {true, 640, 480};
+ encoder.TestInit(param);
+
+ // Create YUV images as source.
+ nsTArray<RefPtr<Image>> images;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ generator.Generate(images);
+
+ // 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(),
+ mozilla::StreamTime(90000),
+ generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE);
+ }
+
+ // track change notification.
+ encoder.SetCurrentFrames(segment);
+
+ // Pull Encoded Data back from encoder.
+ EncodedFrameContainer container;
+ EXPECT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(container)));
+}
+
+// Test encoding a track that has to skip frames.
+TEST(VP8VideoTrackEncoder, SkippedFrames)
+{
+ // Initiate VP8 encoder
+ TestVP8TrackEncoder encoder;
+ InitParam param = {true, 640, 480};
+ encoder.TestInit(param);
+ nsTArray<RefPtr<Image>> images;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+ VideoSegment segment;
+
+ while (images.Length() < 100) {
+ generator.Generate(images);
+ }
+
+ // Pass 100 frames of the shortest possible duration where we don't get
+ // rounding errors between input/output rate.
+ for (uint32_t i = 0; i < 100; ++i) {
+ segment.AppendFrame(images[i].forget(),
+ mozilla::StreamTime(90), // 1ms
+ generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE,
+ false,
+ now + TimeDuration::FromMilliseconds(i));
+ }
+
+ encoder.SetCurrentFrames(segment);
+
+ // End the track.
+ segment.Clear();
+ encoder.NotifyQueuedTrackChanges(nullptr, 0, 0, TrackEventCommand::TRACK_EVENT_ENDED, segment);
+
+ EncodedFrameContainer container;
+ ASSERT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(container)));
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+
+ // Verify total duration being 100 * 1ms = 100ms in terms of 30fps frame
+ // durations (3 * 1/30s).
+ uint64_t totalDuration = 0;
+ for (auto& frame : container.GetEncodedFrames()) {
+ totalDuration += frame->GetDuration();
+ }
+ const uint64_t threeFrames = (PR_USEC_PER_SEC / 30) * 3;
+ EXPECT_EQ(threeFrames, totalDuration);
+}
+
+// EOS test
+TEST(VP8VideoTrackEncoder, EncodeComplete)
+{
+ // Initiate VP8 encoder
+ TestVP8TrackEncoder encoder;
+ InitParam param = {true, 640, 480};
+ encoder.TestInit(param);
+
+ // track end notification.
+ VideoSegment segment;
+ encoder.NotifyQueuedTrackChanges(nullptr, 0, 0, TrackEventCommand::TRACK_EVENT_ENDED, segment);
+
+ // Pull Encoded Data back from encoder. Since we have sent
+ // EOS to encoder, encoder.GetEncodedTrack should return
+ // NS_OK immidiately.
+ EncodedFrameContainer container;
+ EXPECT_TRUE(NS_SUCCEEDED(encoder.GetEncodedTrack(container)));
+}
diff --git a/dom/media/gtest/TestVideoUtils.cpp b/dom/media/gtest/TestVideoUtils.cpp
new file mode 100644
index 0000000000..f803cbb1d1
--- /dev/null
+++ b/dom/media/gtest/TestVideoUtils.cpp
@@ -0,0 +1,80 @@
+/* -*- 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 "nsString.h"
+#include "VideoUtils.h"
+
+using namespace mozilla;
+
+TEST(StringListRange, MakeStringListRange)
+{
+ static const struct
+ {
+ const char* mList;
+ const char* mExpected;
+ } tests[] =
+ {
+ { "", "" },
+ { " ", "" },
+ { ",", "" },
+ { " , ", "" },
+ { "a", "a|" },
+ { " a ", "a|" },
+ { "aa,bb", "aa|bb|" },
+ { " a a , b b ", "a a|b b|" },
+ { " , ,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.mExpected, out.Data());
+ }
+}
+
+TEST(StringListRange, StringListContains)
+{
+ static const struct
+ {
+ const char* mList;
+ const char* mItemToSearch;
+ bool mExpected;
+ } tests[] =
+ {
+ { "", "", false },
+ { "", "a", false },
+ { " ", "a", false },
+ { ",", "a", false },
+ { " , ", "", false },
+ { " , ", "a", false },
+ { "a", "a", true },
+ { "a", "b", false },
+ { " a ", "a", true },
+ { "aa,bb", "aa", true },
+ { "aa,bb", "bb", true },
+ { "aa,bb", "cc", false },
+ { "aa,bb", " aa ", false },
+ { " a a , b b ", "a a", true },
+ { " , ,a 1,, ,b 2,", "a 1", true },
+ { " , ,a 1,, ,b 2,", "b 2", true },
+ { " , ,a 1,, ,b 2,", "", false },
+ { " , ,a 1,, ,b 2,", " ", false },
+ { " , ,a 1,, ,b 2,", "A 1", false },
+ { " , ,A 1,, ,b 2,", "a 1", false }
+ };
+
+ for (const auto& test : tests) {
+ nsCString list(test.mList);
+ nsCString itemToSearch(test.mItemToSearch);
+ EXPECT_EQ(test.mExpected, StringListContains(list, itemToSearch))
+ << "trying to find \"" << itemToSearch.Data()
+ << "\" in \"" << list.Data() << "\"";
+ }
+}
diff --git a/dom/media/gtest/TestWebMBuffered.cpp b/dom/media/gtest/TestWebMBuffered.cpp
new file mode 100644
index 0000000000..4572c57674
--- /dev/null
+++ b/dom/media/gtest/TestWebMBuffered.cpp
@@ -0,0 +1,119 @@
+/* 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..e5fd8c7716
--- /dev/null
+++ b/dom/media/gtest/TestWebMWriter.cpp
@@ -0,0 +1,376 @@
+/* -*- 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 "OpusTrackEncoder.h"
+#include "VP8TrackEncoder.h"
+#include "WebMWriter.h"
+
+using namespace mozilla;
+
+class WebMOpusTrackEncoder : public OpusTrackEncoder
+{
+public:
+ bool TestOpusCreation(int aChannels, int aSamplingRate)
+ {
+ if (NS_SUCCEEDED(Init(aChannels, aSamplingRate))) {
+ return true;
+ }
+ return false;
+ }
+};
+
+class WebMVP8TrackEncoder: public VP8TrackEncoder
+{
+public:
+ explicit WebMVP8TrackEncoder(TrackRate aTrackRate = 90000)
+ : VP8TrackEncoder(aTrackRate) {}
+
+ 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;
+ }
+};
+
+const uint64_t FIXED_DURATION = 1000000;
+const uint32_t FIXED_FRAMESIZE = 500;
+
+class TestWebMWriter: public WebMWriter
+{
+public:
+ explicit TestWebMWriter(int aTrackTypes)
+ : WebMWriter(aTrackTypes),
+ mTimestamp(0)
+ {}
+
+ void SetOpusMetadata(int aChannels, int aSampleRate) {
+ WebMOpusTrackEncoder opusEncoder;
+ EXPECT_TRUE(opusEncoder.TestOpusCreation(aChannels, aSampleRate));
+ RefPtr<TrackMetadataBase> opusMeta = opusEncoder.GetMetadata();
+ SetMetadata(opusMeta);
+ }
+ void SetVP8Metadata(int32_t aWidth, int32_t aHeight, int32_t aDisplayWidth,
+ int32_t aDisplayHeight,TrackRate aTrackRate) {
+ WebMVP8TrackEncoder vp8Encoder;
+ EXPECT_TRUE(vp8Encoder.TestVP8Creation(aWidth, aHeight, aDisplayWidth,
+ aDisplayHeight));
+ RefPtr<TrackMetadataBase> vp8Meta = vp8Encoder.GetMetadata();
+ SetMetadata(vp8Meta);
+ }
+
+ // 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) {
+ EncodedFrameContainer encodedVideoData;
+ nsTArray<uint8_t> frameData;
+ RefPtr<EncodedFrame> videoData = new EncodedFrame();
+ // Create dummy frame data.
+ frameData.SetLength(FIXED_FRAMESIZE);
+ videoData->SetFrameType(aFrameType);
+ videoData->SetTimeStamp(mTimestamp);
+ videoData->SetDuration(aDuration);
+ videoData->SwapInFrameData(frameData);
+ encodedVideoData.AppendEncodedFrame(videoData);
+ WriteEncodedTrack(encodedVideoData, 0);
+ mTimestamp += 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.
+ uint64_t mTimestamp;
+};
+
+TEST(WebMWriter, Metadata)
+{
+ TestWebMWriter writer(ContainerWriter::CREATE_AUDIO_TRACK |
+ ContainerWriter::CREATE_VIDEO_TRACK);
+
+ // 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);
+
+ // Set opus metadata.
+ int channel = 1;
+ int sampleRate = 44100;
+ writer.SetOpusMetadata(channel, sampleRate);
+
+ // No output data since we didn't set both audio/video
+ // metadata in writer.
+ writer.GetContainerData(&encodedBuf, ContainerWriter::GET_HEADER);
+ EXPECT_TRUE(encodedBuf.Length() == 0);
+ writer.GetContainerData(&encodedBuf, ContainerWriter::FLUSH_NEEDED);
+ EXPECT_TRUE(encodedBuf.Length() == 0);
+
+ // Set vp8 metadata
+ int32_t width = 640;
+ int32_t height = 480;
+ int32_t displayWidth = 640;
+ int32_t displayHeight = 480;
+ TrackRate aTrackRate = 90000;
+ writer.SetVP8Metadata(width, height, displayWidth,
+ displayHeight, aTrackRate);
+
+ writer.GetContainerData(&encodedBuf, ContainerWriter::GET_HEADER);
+ EXPECT_TRUE(encodedBuf.Length() > 0);
+}
+
+TEST(WebMWriter, Cluster)
+{
+ TestWebMWriter writer(ContainerWriter::CREATE_AUDIO_TRACK |
+ ContainerWriter::CREATE_VIDEO_TRACK);
+ // Set opus metadata.
+ int channel = 1;
+ int sampleRate = 48000;
+ writer.SetOpusMetadata(channel, sampleRate);
+ // Set vp8 metadata
+ int32_t width = 320;
+ int32_t height = 240;
+ int32_t displayWidth = 320;
+ int32_t displayHeight = 240;
+ TrackRate aTrackRate = 90000;
+ writer.SetVP8Metadata(width, height, displayWidth,
+ displayHeight, aTrackRate);
+
+ 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(ContainerWriter::CREATE_AUDIO_TRACK |
+ ContainerWriter::CREATE_VIDEO_TRACK);
+ // Set opus metadata.
+ int channel = 2;
+ int sampleRate = 44100;
+ writer.SetOpusMetadata(channel, sampleRate);
+ // Set vp8 metadata
+ int32_t width = 176;
+ int32_t height = 352;
+ int32_t displayWidth = 176;
+ int32_t displayHeight = 352;
+ TrackRate aTrackRate = 100000;
+ writer.SetVP8Metadata(width, height, displayWidth,
+ displayHeight, aTrackRate);
+
+ // 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(ContainerWriter::CREATE_AUDIO_TRACK |
+ ContainerWriter::CREATE_VIDEO_TRACK);
+ // Set opus metadata.
+ int channel = 1;
+ int sampleRate = 44100;
+ writer.SetOpusMetadata(channel, sampleRate);
+ // Set vp8 metadata
+ int32_t width = 640;
+ int32_t height = 480;
+ int32_t displayWidth = 1280;
+ int32_t displayHeight = 960;
+ TrackRate aTrackRate = 90000;
+ writer.SetVP8Metadata(width, height, displayWidth,
+ displayHeight, aTrackRate);
+
+ // 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>(sampleRate), params.rate);
+ }
+ }
+ if (context) {
+ nestegg_destroy(context);
+ }
+}
+
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..cd111882ae
--- /dev/null
+++ b/dom/media/gtest/hello.rs
@@ -0,0 +1,6 @@
+#[no_mangle]
+pub extern 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/mediasource_test.mp4 b/dom/media/gtest/mediasource_test.mp4
new file mode 100644
index 0000000000..63707a9146
--- /dev/null
+++ b/dom/media/gtest/mediasource_test.mp4
Binary files differ
diff --git a/dom/media/gtest/moz.build b/dom/media/gtest/moz.build
new file mode 100644
index 0000000000..fc92d5ef3b
--- /dev/null
+++ b/dom/media/gtest/moz.build
@@ -0,0 +1,80 @@
+# -*- 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/.
+
+UNIFIED_SOURCES += [
+ 'MockMediaResource.cpp',
+ 'TestAudioBuffers.cpp',
+ 'TestAudioCompactor.cpp',
+ 'TestAudioMixer.cpp',
+ 'TestAudioPacketizer.cpp',
+ 'TestAudioSegment.cpp',
+ 'TestGMPCrossOrigin.cpp',
+ 'TestGMPRemoveAndDelete.cpp',
+ 'TestGMPUtils.cpp',
+ 'TestIntervalSet.cpp',
+ 'TestMediaDataDecoder.cpp',
+ 'TestMediaEventSource.cpp',
+ 'TestMediaFormatReader.cpp',
+ 'TestMozPromise.cpp',
+ 'TestMP3Demuxer.cpp',
+ 'TestMP4Demuxer.cpp',
+ # 'TestMP4Reader.cpp', disabled so we can turn check tests back on (bug 1175752)
+ 'TestTrackEncoder.cpp',
+ 'TestVideoSegment.cpp',
+ 'TestVideoUtils.cpp',
+ 'TestVPXDecoding.cpp',
+ 'TestWebMBuffered.cpp',
+]
+
+if CONFIG['MOZ_WEBM_ENCODER']:
+ UNIFIED_SOURCES += [
+ 'TestVideoTrackEncoder.cpp',
+ 'TestWebMWriter.cpp',
+ ]
+
+if CONFIG['MOZ_RUST']:
+ UNIFIED_SOURCES += ['TestRust.cpp',]
+
+
+TEST_HARNESS_FILES.gtest += [
+ '../test/gizmo-frag.mp4',
+ '../test/gizmo.mp4',
+ '../test/vp9cake.webm',
+ 'dash_dashinit.mp4',
+ 'id3v2header.mp3',
+ 'mediasource_test.mp4',
+ 'negative_duration.mp4',
+ 'noise.mp3',
+ 'noise_vbr.mp3',
+ 'short-zero-in-moov.mp4',
+ 'short-zero-inband.mov',
+ 'small-shot-false-positive.mp3',
+ 'small-shot.mp3',
+ 'test.webm',
+ 'test_case_1224361.vp8.ivf',
+ 'test_case_1224363.vp8.ivf',
+ 'test_case_1224369.vp8.ivf',
+]
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+LOCAL_INCLUDES += [
+ '/dom/media',
+ '/dom/media/encoder',
+ '/dom/media/fmp4',
+ '/dom/media/gmp',
+ '/security/certverifier',
+ '/security/pkix/include',
+]
+
+FINAL_LIBRARY = 'xul-gtest'
+
+if CONFIG['GNU_CXX']:
+ CXXFLAGS += ['-Wno-error=shadow']
+
+USE_LIBS += [
+ 'rlz',
+]
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.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