/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "gtest/gtest.h" #include "AnnexB.h" #include "ImageContainer.h" #include "mozilla/AbstractThread.h" #include "mozilla/SpinEventLoopUntil.h" #include "mozilla/media/MediaUtils.h" // For media::Await #include "nsMimeTypes.h" #include "PEMFactory.h" #include "TimeUnits.h" #include "VideoUtils.h" #include #include #define SKIP_IF_NOT_SUPPORTED(mimeType) \ do { \ RefPtr f(new PEMFactory()); \ if (!f->SupportsMimeType(nsLiteralCString(mimeType))) { \ return; \ } \ } while (0) #define BLOCK_SIZE 64 #define WIDTH 640 #define HEIGHT 480 #define NUM_FRAMES 150UL #define FRAME_RATE 30 #define FRAME_DURATION (1000000 / FRAME_RATE) #define BIT_RATE (1000 * 1000) // 1Mbps #define KEYFRAME_INTERVAL FRAME_RATE // 1 keyframe per second using namespace mozilla; static gfx::IntSize kImageSize(WIDTH, HEIGHT); class MediaDataEncoderTest : public testing::Test { protected: void SetUp() override { mData.Init(kImageSize); } void TearDown() override { mData.Deinit(); } public: struct FrameSource final { layers::PlanarYCbCrData mYUV; UniquePtr mBuffer; RefPtr mRecycleBin; int16_t mColorStep = 4; void Init(const gfx::IntSize& aSize) { mYUV.mPicSize = aSize; mYUV.mYStride = aSize.width; mYUV.mYSize = aSize; mYUV.mCbCrStride = aSize.width / 2; mYUV.mCbCrSize = gfx::IntSize(aSize.width / 2, aSize.height / 2); size_t bufferSize = mYUV.mYStride * mYUV.mYSize.height + mYUV.mCbCrStride * mYUV.mCbCrSize.height + mYUV.mCbCrStride * mYUV.mCbCrSize.height; mBuffer = MakeUnique(bufferSize); std::fill_n(mBuffer.get(), bufferSize, 0x7F); mYUV.mYChannel = mBuffer.get(); mYUV.mCbChannel = mYUV.mYChannel + mYUV.mYStride * mYUV.mYSize.height; mYUV.mCrChannel = mYUV.mCbChannel + mYUV.mCbCrStride * mYUV.mCbCrSize.height; mRecycleBin = new layers::BufferRecycleBin(); } void Deinit() { mBuffer.reset(); mRecycleBin = nullptr; } already_AddRefed GetFrame(const size_t aIndex) { Draw(aIndex); RefPtr img = new layers::RecyclingPlanarYCbCrImage(mRecycleBin); img->CopyData(mYUV); RefPtr frame = VideoData::CreateFromImage( kImageSize, 0, media::TimeUnit::FromMicroseconds(aIndex * FRAME_DURATION), media::TimeUnit::FromMicroseconds(FRAME_DURATION), img, (aIndex & 0xF) == 0, media::TimeUnit::FromMicroseconds(aIndex * FRAME_DURATION)); return frame.forget(); } void DrawChessboard(uint8_t* aAddr, const size_t aWidth, const size_t aHeight, const size_t aOffset) { uint8_t pixels[2][BLOCK_SIZE]; size_t x = aOffset % BLOCK_SIZE; if ((aOffset / BLOCK_SIZE) & 1) { x = BLOCK_SIZE - x; } for (size_t i = 0; i < x; i++) { pixels[0][i] = 0x00; pixels[1][i] = 0xFF; } for (size_t i = x; i < BLOCK_SIZE; i++) { pixels[0][i] = 0xFF; pixels[1][i] = 0x00; } uint8_t* p = aAddr; for (size_t row = 0; row < aHeight; row++) { for (size_t col = 0; col < aWidth; col += BLOCK_SIZE) { memcpy(p, pixels[((row / BLOCK_SIZE) + (col / BLOCK_SIZE)) % 2], BLOCK_SIZE); p += BLOCK_SIZE; } } } void Draw(const size_t aIndex) { DrawChessboard(mYUV.mYChannel, mYUV.mYSize.width, mYUV.mYSize.height, aIndex << 1); int16_t color = mYUV.mCbChannel[0] + mColorStep; if (color > 255 || color < 0) { mColorStep = -mColorStep; color = mYUV.mCbChannel[0] + mColorStep; } size_t size = (mYUV.mCrChannel - mYUV.mCbChannel); std::fill_n(mYUV.mCbChannel, size, static_cast(color)); std::fill_n(mYUV.mCrChannel, size, 0xFF - static_cast(color)); } }; public: FrameSource mData; }; static already_AddRefed CreateH264Encoder( MediaDataEncoder::Usage aUsage = MediaDataEncoder::Usage::Realtime, MediaDataEncoder::PixelFormat aPixelFormat = MediaDataEncoder::PixelFormat::YUV420P, int32_t aWidth = WIDTH, int32_t aHeight = HEIGHT, const Maybe& aSpecific = Some(MediaDataEncoder::H264Specific( KEYFRAME_INTERVAL, MediaDataEncoder::H264Specific::ProfileLevel::BaselineAutoLevel))) { RefPtr f(new PEMFactory()); if (!f->SupportsMimeType(nsLiteralCString(VIDEO_MP4))) { return nullptr; } VideoInfo videoInfo(aWidth, aHeight); videoInfo.mMimeType = nsLiteralCString(VIDEO_MP4); const RefPtr taskQueue( new TaskQueue(GetMediaThreadPool(MediaThreadType::PLATFORM_ENCODER))); RefPtr e; if (aSpecific) { e = f->CreateEncoder(CreateEncoderParams( videoInfo /* track info */, aUsage, taskQueue, aPixelFormat, FRAME_RATE /* FPS */, BIT_RATE /* bitrate */, aSpecific.value())); } else { e = f->CreateEncoder(CreateEncoderParams( videoInfo /* track info */, aUsage, taskQueue, aPixelFormat, FRAME_RATE /* FPS */, BIT_RATE /* bitrate */)); } return e.forget(); } void WaitForShutdown(RefPtr aEncoder) { MOZ_ASSERT(aEncoder); Maybe result; // media::Await() supports exclusive promises only, but ShutdownPromise is // not. aEncoder->Shutdown()->Then( AbstractThread::MainThread(), __func__, [&result](bool rv) { EXPECT_TRUE(rv); result = Some(true); }, [&result]() { FAIL() << "Shutdown should never be rejected"; result = Some(false); }); SpinEventLoopUntil([&result]() { return result; }); } TEST_F(MediaDataEncoderTest, H264Create) { SKIP_IF_NOT_SUPPORTED(VIDEO_MP4); RefPtr e = CreateH264Encoder(); EXPECT_TRUE(e); WaitForShutdown(e); } static bool EnsureInit(RefPtr aEncoder) { if (!aEncoder) { return false; } bool succeeded; media::Await( GetMediaThreadPool(MediaThreadType::SUPERVISOR), aEncoder->Init(), [&succeeded](TrackInfo::TrackType t) { EXPECT_EQ(TrackInfo::TrackType::kVideoTrack, t); succeeded = true; }, [&succeeded](MediaResult r) { succeeded = false; }); return succeeded; } TEST_F(MediaDataEncoderTest, H264InitWithoutSpecific) { SKIP_IF_NOT_SUPPORTED(VIDEO_MP4); RefPtr e = CreateH264Encoder( MediaDataEncoder::Usage::Realtime, MediaDataEncoder::PixelFormat::YUV420P, WIDTH, HEIGHT, Nothing()); #if defined(MOZ_WIDGET_ANDROID) // Android encoder requires I-frame interval EXPECT_FALSE(EnsureInit(e)); #else EXPECT_TRUE(EnsureInit(e)); #endif WaitForShutdown(e); } TEST_F(MediaDataEncoderTest, H264Init) { SKIP_IF_NOT_SUPPORTED(VIDEO_MP4); RefPtr e = CreateH264Encoder(); EXPECT_TRUE(EnsureInit(e)); WaitForShutdown(e); } static MediaDataEncoder::EncodedData Encode( const RefPtr aEncoder, const size_t aNumFrames, MediaDataEncoderTest::FrameSource& aSource) { MediaDataEncoder::EncodedData output; bool succeeded; for (size_t i = 0; i < aNumFrames; i++) { RefPtr frame = aSource.GetFrame(i); media::Await( GetMediaThreadPool(MediaThreadType::SUPERVISOR), aEncoder->Encode(frame), [&output, &succeeded](MediaDataEncoder::EncodedData encoded) { output.AppendElements(std::move(encoded)); succeeded = true; }, [&succeeded](MediaResult r) { succeeded = false; }); EXPECT_TRUE(succeeded); if (!succeeded) { return output; } } size_t pending = 0; do { media::Await( GetMediaThreadPool(MediaThreadType::SUPERVISOR), aEncoder->Drain(), [&pending, &output, &succeeded](MediaDataEncoder::EncodedData encoded) { pending = encoded.Length(); output.AppendElements(std::move(encoded)); succeeded = true; }, [&succeeded](MediaResult r) { succeeded = false; }); EXPECT_TRUE(succeeded); if (!succeeded) { return output; } } while (pending > 0); return output; } TEST_F(MediaDataEncoderTest, H264EncodeOneFrameAsAnnexB) { SKIP_IF_NOT_SUPPORTED(VIDEO_MP4); RefPtr e = CreateH264Encoder(); EnsureInit(e); MediaDataEncoder::EncodedData output = Encode(e, 1UL, mData); EXPECT_EQ(output.Length(), 1UL); EXPECT_TRUE(AnnexB::IsAnnexB(output[0])); WaitForShutdown(e); } TEST_F(MediaDataEncoderTest, EncodeMultipleFramesAsAnnexB) { SKIP_IF_NOT_SUPPORTED(VIDEO_MP4); RefPtr e = CreateH264Encoder(); EnsureInit(e); MediaDataEncoder::EncodedData output = Encode(e, NUM_FRAMES, mData); EXPECT_EQ(output.Length(), NUM_FRAMES); for (auto frame : output) { EXPECT_TRUE(AnnexB::IsAnnexB(frame)); } WaitForShutdown(e); } TEST_F(MediaDataEncoderTest, EncodeMultipleFramesAsAVCC) { SKIP_IF_NOT_SUPPORTED(VIDEO_MP4); RefPtr e = CreateH264Encoder(MediaDataEncoder::Usage::Record); EnsureInit(e); MediaDataEncoder::EncodedData output = Encode(e, NUM_FRAMES, mData); EXPECT_EQ(output.Length(), NUM_FRAMES); AnnexB::IsAVCC(output[0]); // Only 1st frame has extra data. for (auto frame : output) { EXPECT_FALSE(AnnexB::IsAnnexB(frame)); } WaitForShutdown(e); } #ifndef DEBUG // Zero width or height will assert/crash in debug builds. TEST_F(MediaDataEncoderTest, InvalidSize) { SKIP_IF_NOT_SUPPORTED(VIDEO_MP4); RefPtr e0x0 = CreateH264Encoder(MediaDataEncoder::Usage::Realtime, MediaDataEncoder::PixelFormat::YUV420P, 0, 0); EXPECT_NE(e0x0, nullptr); EXPECT_FALSE(EnsureInit(e0x0)); RefPtr e0x1 = CreateH264Encoder(MediaDataEncoder::Usage::Realtime, MediaDataEncoder::PixelFormat::YUV420P, 0, 1); EXPECT_NE(e0x1, nullptr); EXPECT_FALSE(EnsureInit(e0x1)); RefPtr e1x0 = CreateH264Encoder(MediaDataEncoder::Usage::Realtime, MediaDataEncoder::PixelFormat::YUV420P, 1, 0); EXPECT_NE(e1x0, nullptr); EXPECT_FALSE(EnsureInit(e1x0)); } #endif #ifdef MOZ_WIDGET_ANDROID TEST_F(MediaDataEncoderTest, AndroidNotSupportedSize) { SKIP_IF_NOT_SUPPORTED(VIDEO_MP4); RefPtr e = CreateH264Encoder(MediaDataEncoder::Usage::Realtime, MediaDataEncoder::PixelFormat::YUV420P, 1, 1); EXPECT_NE(e, nullptr); EXPECT_FALSE(EnsureInit(e)); } #endif