diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /dom/media/gtest | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'dom/media/gtest')
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 000000000..a55f8fb68 --- /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 000000000..8ce6f8ddd --- /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 000000000..324f18141 --- /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 000000000..4e75f8d9d --- /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 000000000..129ba1b72 --- /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 000000000..b03886cec --- /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 000000000..9a28254b3 --- /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 000000000..4e075a599 --- /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 000000000..86615eb10 --- /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 000000000..968b4b5a1 --- /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 000000000..036282153 --- /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 000000000..4ac92e34e --- /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 000000000..75d7eba53 --- /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 000000000..b054ba1d2 --- /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 000000000..8d2109f00 --- /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 000000000..f1210fde7 --- /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 000000000..f08f7a40d --- /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 000000000..47e78bf7e --- /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 000000000..75fb09f20 --- /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 000000000..ad222e8b8 --- /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 000000000..b1d363bd5 --- /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 000000000..86d0e99b8 --- /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 000000000..605e0e90c --- /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 000000000..01d5c86e9 --- /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 000000000..d14c4d24e --- /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 000000000..f7d5aed21 --- /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 000000000..65826c9b4 --- /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 000000000..f803cbb1d --- /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 000000000..4572c5767 --- /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 000000000..e5fd8c771 --- /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, ¶ms); + EXPECT_EQ(rv, 0); + EXPECT_EQ(width, static_cast<int32_t>(params.width)); + EXPECT_EQ(height, static_cast<int32_t>(params.height)); + EXPECT_EQ(displayWidth, static_cast<int32_t>(params.display_width)); + EXPECT_EQ(displayHeight, static_cast<int32_t>(params.display_height)); + } else if (type == NESTEGG_TRACK_AUDIO) { + nestegg_audio_params params; + rv = nestegg_track_audio_params(context, track, ¶ms); + EXPECT_EQ(rv, 0); + EXPECT_EQ(channel, static_cast<int>(params.channels)); + EXPECT_EQ(static_cast<double>(sampleRate), params.rate); + } + } + if (context) { + nestegg_destroy(context); + } +} + diff --git a/dom/media/gtest/dash_dashinit.mp4 b/dom/media/gtest/dash_dashinit.mp4 Binary files differnew file mode 100644 index 000000000..d19068f36 --- /dev/null +++ b/dom/media/gtest/dash_dashinit.mp4 diff --git a/dom/media/gtest/hello.rs b/dom/media/gtest/hello.rs new file mode 100644 index 000000000..cd111882a --- /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 Binary files differnew file mode 100644 index 000000000..2f5585d02 --- /dev/null +++ b/dom/media/gtest/id3v2header.mp3 diff --git a/dom/media/gtest/mediasource_test.mp4 b/dom/media/gtest/mediasource_test.mp4 Binary files differnew file mode 100644 index 000000000..63707a914 --- /dev/null +++ b/dom/media/gtest/mediasource_test.mp4 diff --git a/dom/media/gtest/moz.build b/dom/media/gtest/moz.build new file mode 100644 index 000000000..fc92d5ef3 --- /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 Binary files differnew file mode 100644 index 000000000..de86bf497 --- /dev/null +++ b/dom/media/gtest/negative_duration.mp4 diff --git a/dom/media/gtest/noise.mp3 b/dom/media/gtest/noise.mp3 Binary files differnew file mode 100644 index 000000000..e76b50350 --- /dev/null +++ b/dom/media/gtest/noise.mp3 diff --git a/dom/media/gtest/noise_vbr.mp3 b/dom/media/gtest/noise_vbr.mp3 Binary files differnew file mode 100644 index 000000000..284ebe40b --- /dev/null +++ b/dom/media/gtest/noise_vbr.mp3 diff --git a/dom/media/gtest/short-zero-in-moov.mp4 b/dom/media/gtest/short-zero-in-moov.mp4 Binary files differnew file mode 100644 index 000000000..577318c8f --- /dev/null +++ b/dom/media/gtest/short-zero-in-moov.mp4 diff --git a/dom/media/gtest/short-zero-inband.mov b/dom/media/gtest/short-zero-inband.mov Binary files differnew file mode 100644 index 000000000..9c1864286 --- /dev/null +++ b/dom/media/gtest/short-zero-inband.mov diff --git a/dom/media/gtest/small-shot-false-positive.mp3 b/dom/media/gtest/small-shot-false-positive.mp3 Binary files differnew file mode 100644 index 000000000..2f1e79405 --- /dev/null +++ b/dom/media/gtest/small-shot-false-positive.mp3 diff --git a/dom/media/gtest/small-shot.mp3 b/dom/media/gtest/small-shot.mp3 Binary files differnew file mode 100644 index 000000000..f9397a510 --- /dev/null +++ b/dom/media/gtest/small-shot.mp3 diff --git a/dom/media/gtest/test.webm b/dom/media/gtest/test.webm Binary files differnew file mode 100644 index 000000000..487914c4a --- /dev/null +++ b/dom/media/gtest/test.webm diff --git a/dom/media/gtest/test_case_1224361.vp8.ivf b/dom/media/gtest/test_case_1224361.vp8.ivf Binary files differnew file mode 100644 index 000000000..e2fe942f0 --- /dev/null +++ b/dom/media/gtest/test_case_1224361.vp8.ivf diff --git a/dom/media/gtest/test_case_1224363.vp8.ivf b/dom/media/gtest/test_case_1224363.vp8.ivf Binary files differnew file mode 100644 index 000000000..6d2e4e020 --- /dev/null +++ b/dom/media/gtest/test_case_1224363.vp8.ivf diff --git a/dom/media/gtest/test_case_1224369.vp8.ivf b/dom/media/gtest/test_case_1224369.vp8.ivf Binary files differnew file mode 100644 index 000000000..2f8deb114 --- /dev/null +++ b/dom/media/gtest/test_case_1224369.vp8.ivf |