/* 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 "ImageContainer.h" #include "MediaData.h" #include "MediaInfo.h" #include "MediaResult.h" #include "gtest/gtest.h" #include "mozilla/AutoRestore.h" #include "mozilla/ResultVariant.h" #include "mozilla/gfx/Types.h" #include "nsIWidget.h" #include "nsTArray.h" using namespace mozilla; using namespace mozilla::gfx; using namespace mozilla::layers; using media::TimeUnit; static void BuildI420Buffer(uint32_t aWidth, uint32_t aHeight, nsTArray& aStorage, VideoData::YCbCrBuffer& aBuffer) { uint32_t yLen = aWidth * aHeight; uint32_t uvW = (aWidth + 1) / 2; uint32_t uvH = (aHeight + 1) / 2; uint32_t uvLen = uvW * uvH; aStorage.SetLength(yLen + 2 * uvLen); memset(aStorage.Elements(), 0x10, yLen); memset(aStorage.Elements() + yLen, 0x80, 2 * uvLen); aBuffer.mPlanes[0] = {aStorage.Elements(), aWidth, aHeight, aWidth, 0}; aBuffer.mPlanes[1] = {aStorage.Elements() + yLen, uvW, uvH, uvW, 0}; aBuffer.mPlanes[2] = {aStorage.Elements() + yLen + uvLen, uvW, uvH, uvW, 0}; aBuffer.mYUVColorSpace = YUVColorSpace::BT2020; aBuffer.mChromaSubsampling = ChromaSubsampling::HALF_WIDTH_AND_HEIGHT; } // Runs SetVideoDataToImage with the given HDR transfer function and full // HDRMetadata (Smpte2086 + ContentLightLevel), then asserts all attributes. static void TestHDRTransferFunctionPropagation(TransferFunction aTF) { const uint32_t w = 64, h = 48; nsTArray storage; VideoData::YCbCrBuffer buf{}; BuildI420Buffer(w, h, storage, buf); VideoInfo info; info.mDisplay = IntSize(w, h); info.mTransferFunction = Some(aTF); gfx::Smpte2086Metadata smpte2086; smpte2086.displayPrimaryRed = {0.708f, 0.292f}; smpte2086.displayPrimaryGreen = {0.170f, 0.797f}; smpte2086.displayPrimaryBlue = {0.131f, 0.046f}; smpte2086.whitePoint = {0.3127f, 0.3290f}; smpte2086.maxLuminance = 1000.0f; smpte2086.minLuminance = 0.001f; gfx::HDRMetadata hdrMetadata; hdrMetadata.mSmpte2086 = Some(smpte2086); hdrMetadata.mContentLightLevel = Some(gfx::ContentLightLevel{1000, 400}); info.mHDRMetadata = Some(hdrMetadata); RefPtr image = new RecyclingPlanarYCbCrImage(new BufferRecycleBin()); MediaResult rv = VideoData::SetVideoDataToImage(image, info, buf, IntRect(0, 0, w, h), true); ASSERT_TRUE(NS_SUCCEEDED(rv)); const PlanarYCbCrData* data = image->GetData(); ASSERT_NE(data, nullptr); EXPECT_EQ(data->mTransferFunction, aTF); ASSERT_TRUE(data->mHDRMetadata.isSome()); ASSERT_TRUE(data->mHDRMetadata->mSmpte2086.isSome()); EXPECT_FLOAT_EQ(data->mHDRMetadata->mSmpte2086->maxLuminance, 1000.0f); EXPECT_FLOAT_EQ(data->mHDRMetadata->mSmpte2086->minLuminance, 0.001f); EXPECT_FLOAT_EQ(data->mHDRMetadata->mSmpte2086->displayPrimaryRed.x, 0.708f); EXPECT_FLOAT_EQ(data->mHDRMetadata->mSmpte2086->whitePoint.y, 0.3290f); ASSERT_TRUE(data->mHDRMetadata->mContentLightLevel.isSome()); EXPECT_EQ(data->mHDRMetadata->mContentLightLevel->maxContentLightLevel, 1000); EXPECT_EQ(data->mHDRMetadata->mContentLightLevel->maxFrameAverageLightLevel, 400); } TEST(VideoData, SetVideoDataToImagePropagatesPQTransferFunction) { TestHDRTransferFunctionPropagation(TransferFunction::PQ); } TEST(VideoData, SetVideoDataToImagePropagatesHLGTransferFunction) { TestHDRTransferFunctionPropagation(TransferFunction::HLG); } // When VideoInfo has no TransferFunction, the BT709 default is preserved. TEST(VideoData, SetVideoDataToImagePreservesBT709Default) { const uint32_t w = 64, h = 48; nsTArray storage; VideoData::YCbCrBuffer buf{}; BuildI420Buffer(w, h, storage, buf); VideoInfo info; info.mDisplay = IntSize(w, h); // mTransferFunction = Nothing() — default RefPtr image = new RecyclingPlanarYCbCrImage(new BufferRecycleBin()); MediaResult rv = VideoData::SetVideoDataToImage(image, info, buf, IntRect(0, 0, w, h), true); ASSERT_TRUE(NS_SUCCEEDED(rv)); const PlanarYCbCrData* data = image->GetData(); ASSERT_NE(data, nullptr); EXPECT_EQ(data->mTransferFunction, TransferFunction::BT709); EXPECT_EQ(data->mHDRMetadata, Nothing()); } TEST(VideoData, StrideOrSizeMismatch) { constexpr int width = 32; constexpr int height = 64; constexpr int stride = width * 10; VideoInfo info(width, height); uint8_t buffer[stride * height] = {}; VideoData::YCbCrBuffer b; VideoData::YCbCrBuffer::Plane alpha_plane; b.mPlanes[0].mStride = alpha_plane.mStride = stride; b.mPlanes[0].mHeight = alpha_plane.mHeight = height; b.mPlanes[0].mWidth = alpha_plane.mWidth = width; b.mPlanes[0].mSkip = alpha_plane.mSkip = 0; b.mPlanes[0].mData = alpha_plane.mData = buffer; b.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT; b.mPlanes[1].mStride = b.mPlanes[2].mStride = stride / 4; b.mPlanes[1].mSkip = b.mPlanes[2].mSkip = 0; b.mPlanes[1].mHeight = b.mPlanes[2].mHeight = (b.mPlanes[0].mHeight + 1) / 2; b.mPlanes[1].mWidth = b.mPlanes[2].mWidth = (b.mPlanes[0].mWidth + 1) / 2; // Use tail end of buffer. b.mPlanes[1].mData = b.mPlanes[2].mData = buffer + stride * height - b.mPlanes[1].mStride * b.mPlanes[1].mHeight; b.mYUVColorSpace = gfx::YUVColorSpace::BT601; b.mColorRange = gfx::ColorRange::FULL; RefPtr imageContainer = new ImageContainer( ImageUsageType::VideoFrameContainer, ImageContainer::ASYNCHRONOUS); auto CreateVideoData = [&]() { Result res = VideoData::CreateAndCopyData( info, imageContainer, /*aOffset*/ 0, TimeUnit::Zero(), TimeUnit::FromSeconds(1), b, alpha_plane, /*aKeyFrame*/ true, TimeUnit::Zero(), info.ImageRect()); return res.unwrapOr(nullptr); }; // Trigger gfx::GPUProcessManager::EnsureImageBridgeChild() // for ImageContainer::CreateSharedRGBImage(), // from VideoData::CreateAndCopyData(). // (ImageContainer::EnsureRecycleAllocatorForRDD() requires // XRE_IsRDDProcess().) RefPtr widget = nsIWidget::CreateHeadlessWidget(); widget->CreateCompositor(); // Check that VideoData can be created successfully from a simple // combination of planes. RefPtr v = CreateVideoData(); EXPECT_NE(v.get(), nullptr); // Check that non-so-simple combinations do not cause crashes. // At the time of writing this test, CreateAndCopyData() returns failure, // but these combinations could be supported in the future, if desired. { // Differing alpha and luma plane strides AutoRestore r(alpha_plane.mStride); AutoRestore r2(alpha_plane.mData); alpha_plane.mStride = width; EXPECT_NE(alpha_plane.mStride, b.mPlanes[0].mStride); // tail end of buffer alpha_plane.mData = buffer + (stride - alpha_plane.mStride) * height; v = CreateVideoData(); // Change to NE if this becomes supported. EXPECT_EQ(v.get(), nullptr); } { // Differing alpha and luma plane heights AutoRestore r(alpha_plane.mHeight); AutoRestore r2(alpha_plane.mData); alpha_plane.mHeight = height / 2; EXPECT_NE(alpha_plane.mHeight, b.mPlanes[0].mHeight); alpha_plane.mData = buffer + stride * height - alpha_plane.mStride * alpha_plane.mHeight; v = CreateVideoData(); // Change to NE if this becomes supported. EXPECT_EQ(v.get(), nullptr); } { // Differing chroma plane strides AutoRestore r(b.mPlanes[2].mStride); AutoRestore r2(b.mPlanes[2].mData); b.mPlanes[2].mStride = b.mPlanes[2].mWidth; EXPECT_NE(b.mPlanes[1].mStride, b.mPlanes[2].mStride); b.mPlanes[2].mData = buffer + stride * height - b.mPlanes[2].mStride * b.mPlanes[2].mHeight; v = CreateVideoData(); // Change to NE if this becomes supported. EXPECT_EQ(v.get(), nullptr); } }