Source code
Revision control
Copy as Markdown
Other Tools
/* 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
#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<uint8_t>& 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<uint8_t> 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<PlanarYCbCrImage> 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<uint8_t> storage;
VideoData::YCbCrBuffer buf{};
BuildI420Buffer(w, h, storage, buf);
VideoInfo info;
info.mDisplay = IntSize(w, h);
// mTransferFunction = Nothing() — default
RefPtr<PlanarYCbCrImage> 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);
}
}
// For a 16-bit buffer (e.g. P010/P016) the luma stride in bytes is twice the
// width in samples. A picture region whose width is greater than the frame
// width must be rejected, since the picture region and the frame width are both
// measured in samples.
TEST(VideoData, PictureRegionExceedingFrameWidth)
{
constexpr int width = 560;
constexpr int height = 16;
// 16-bit samples: luma stride in bytes is twice the width in samples.
constexpr int yStride = width * 2;
constexpr int uvWidth = (width + 1) / 2;
constexpr int uvHeight = (height + 1) / 2;
constexpr int uvStride = uvWidth * 2;
// A picture width greater than the frame width.
constexpr int picWidth = yStride - 8;
VideoInfo info(width, height);
// Size the backing storage for the full picture extent.
nsTArray<uint8_t> yStorage;
yStorage.SetLength(static_cast<size_t>(yStride) * height);
memset(yStorage.Elements(), 0x10, yStorage.Length());
nsTArray<uint8_t> cbStorage;
nsTArray<uint8_t> crStorage;
cbStorage.SetLength(static_cast<size_t>(uvStride) * uvHeight);
crStorage.SetLength(static_cast<size_t>(uvStride) * uvHeight);
memset(cbStorage.Elements(), 0x80, cbStorage.Length());
memset(crStorage.Elements(), 0x80, crStorage.Length());
VideoData::YCbCrBuffer b;
b.mColorDepth = gfx::ColorDepth::COLOR_16;
b.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
b.mYUVColorSpace = gfx::YUVColorSpace::BT601;
b.mColorRange = gfx::ColorRange::FULL;
b.mPlanes[0] = {yStorage.Elements(), width, height, yStride, 0};
b.mPlanes[1] = {cbStorage.Elements(), uvWidth, uvHeight, uvStride, 0};
b.mPlanes[2] = {crStorage.Elements(), uvWidth, uvHeight, uvStride, 0};
RefPtr imageContainer = new ImageContainer(
ImageUsageType::VideoFrameContainer, ImageContainer::ASYNCHRONOUS);
RefPtr widget = nsIWidget::CreateHeadlessWidget();
widget->CreateCompositor();
// A picture region contained in the frame is accepted, confirming the buffer
// setup is otherwise valid.
IntRect validPicture(0, 0, width, height);
RefPtr valid = VideoData::CreateAndCopyData(
info, imageContainer, /*aOffset*/ 0, TimeUnit::Zero(),
TimeUnit::FromSeconds(1), b, /*aKeyFrame*/ true,
TimeUnit::Zero(), validPicture, /*aAllocator*/ nullptr)
.unwrapOr(nullptr);
EXPECT_NE(valid.get(), nullptr);
// A picture region wider than the frame is rejected with NS_ERROR_INVALID_ARG
// during validation.
IntRect picture(0, 0, picWidth, height);
Result res = VideoData::CreateAndCopyData(
info, imageContainer, /*aOffset*/ 0, TimeUnit::Zero(),
TimeUnit::FromSeconds(1), b, /*aKeyFrame*/ true, TimeUnit::Zero(),
picture,
/*aAllocator*/ nullptr);
ASSERT_TRUE(res.isErr());
EXPECT_EQ(res.inspectErr().Code(), NS_ERROR_INVALID_ARG);
}
namespace {
// Records whether the pixel copy runs; the copy itself is a no-op so the test
// exercises SetVideoDataToImage's picture-rect handling without touching
// pixels.
class NoCopyPlanarYCbCrImage final : public PlanarYCbCrImage {
public:
NoCopyPlanarYCbCrImage() { mSize = IntSize(0, 0); }
nsresult CopyData(const Data&) override {
mCopyDataCalled = true;
return NS_OK;
}
size_t SizeOfExcludingThis(mozilla::MallocSizeOf) const { return 0; }
bool mCopyDataCalled = false;
};
} // namespace
TEST(VideoData, SetVideoDataToImagePictureRect)
{
const uint32_t w = 320, h = 240;
nsTArray<uint8_t> storage;
VideoData::YCbCrBuffer buf{};
BuildI420Buffer(w, h, storage, buf);
VideoInfo info(w, h);
// A picture rect that fits the buffer is accepted and the copy runs.
RefPtr<NoCopyPlanarYCbCrImage> fitImage = new NoCopyPlanarYCbCrImage();
MediaResult fit = VideoData::SetVideoDataToImage(fitImage, info, buf,
IntRect(0, 0, w, h), true);
EXPECT_TRUE(NS_SUCCEEDED(fit));
EXPECT_TRUE(fitImage->mCopyDataCalled);
// A picture rect larger than the buffer is not accepted.
RefPtr<NoCopyPlanarYCbCrImage> oversizedImage = new NoCopyPlanarYCbCrImage();
MediaResult oversized = VideoData::SetVideoDataToImage(
oversizedImage, info, buf, IntRect(0, 0, w, h * 4), true);
EXPECT_TRUE(NS_FAILED(oversized));
EXPECT_FALSE(oversizedImage->mCopyDataCalled);
}
TEST(VideoInfo, AdoptImageSize)
{
// An unchanged image size keeps an existing sub-crop.
{
VideoInfo info(320, 240);
info.SetImageRect(IntRect(10, 10, 300, 220));
info.AdoptImageSize(IntSize(320, 240));
EXPECT_EQ(info.ImageRect(), IntRect(10, 10, 300, 220));
}
// A changed image size discards the previous picture rectangle.
{
VideoInfo info(320, 4096);
info.SetImageRect(IntRect(0, 0, 320, 4096));
info.AdoptImageSize(IntSize(320, 240));
EXPECT_EQ(info.ImageRect(), IntRect(0, 0, 320, 240));
}
}