/* 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/. */ #import #import #include "gtest/gtest.h" #include "mozilla/ScopeExit.h" #include "mozilla/TypedEnumBits.h" #include "mozilla/gfx/MacIOSurface.h" #include "mozilla/layers/NativeLayerCA.h" using namespace mozilla; using namespace mozilla::gfx; using namespace mozilla::layers; static const IntSize kVideoSize(64, 32); // Properties of a presented frame. enum class PresentFlags : uint8_t { None = 0, DRM = 1 << 0, HDR = 1 << 1, }; MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(PresentFlags) // A single surface-presentation layer committed into a detached root CALayer, // either as the onscreen tree or as the offscreen tree used for snapshots. class NativeLayerCAVideo : public ::testing::TestWithParam { protected: void SetUp() override { mRootCALayer = [[CALayer layer] retain]; mOffscreenRootCALayer = [[CALayer layer] retain]; mRoot = NativeLayerRootCA::CreateForCALayer(mRootCALayer); mLayer = mRoot->CreateLayerForSurfacePresentation(kVideoSize, true); mLayer->SetDisplayRect(IntRect(IntPoint(), kVideoSize)); mRoot->AppendLayer(mLayer); } void TearDown() override { mRoot->RemoveLayer(mLayer); mLayer = nullptr; mRoot = nullptr; [mOffscreenRootCALayer release]; [mRootCALayer release]; } bool IsOffscreen() const { return GetParam(); } void Present(const RefPtr& aSurface, PresentFlags aFlags) { ASSERT_TRUE(aSurface); IntSize size = kVideoSize; mLayer->SetSurfaceToPresent(aSurface->GetIOSurfaceRef(), size, bool(aFlags & PresentFlags::DRM), bool(aFlags & PresentFlags::HDR)); if (IsOffscreen()) { mRoot->CommitOffscreen(mOffscreenRootCALayer); } else { mRoot->CommitToScreen(); } } AVSampleBufferDisplayLayer* VideoLayer() { return FindVideoLayer(IsOffscreen() ? mOffscreenRootCALayer : mRootCALayer); } static RefPtr CreateNV12Surface() { return MacIOSurface::CreateBiPlanarSurface( kVideoSize, IntSize(kVideoSize.width / 2, kVideoSize.height / 2), ChromaSubsampling::HALF_WIDTH_AND_HEIGHT, YUVColorSpace::BT709, TransferFunction::BT709, ColorRange::LIMITED, ColorDepth::COLOR_8, MacIOSurface::AllowAlpha::No); } static RefPtr CreateBGRASurface() { return MacIOSurface::CreateIOSurface(kVideoSize.width, kVideoSize.height, MacIOSurface::AllowAlpha::No); } static AVSampleBufferDisplayLayer* FindVideoLayer(CALayer* aLayer) { if ([aLayer isKindOfClass:[AVSampleBufferDisplayLayer class]]) { return static_cast(aLayer); } for (CALayer* sublayer in aLayer.sublayers) { if (AVSampleBufferDisplayLayer* videoLayer = FindVideoLayer(sublayer)) { return videoLayer; } } return nil; } CALayer* mRootCALayer = nil; CALayer* mOffscreenRootCALayer = nil; RefPtr mRoot; RefPtr mLayer; }; TEST_P(NativeLayerCAVideo, DRMVideoLayerIsCaptureProtected) { // Present a DRM video frame; the video layer must block capture and hold // no frame in its contents. ASSERT_NO_FATAL_FAILURE(Present(CreateNV12Surface(), PresentFlags::DRM)); ASSERT_NE(VideoLayer(), nil); EXPECT_TRUE(VideoLayer().preventsCapture); EXPECT_EQ(VideoLayer().contents, nil); } TEST_P(NativeLayerCAVideo, RebuiltDRMVideoLayerIsCaptureProtected) { // Present a DRM video frame; DRM video always uses a video layer, so one is // built. ASSERT_NO_FATAL_FAILURE(Present(CreateNV12Surface(), PresentFlags::DRM)); ASSERT_NE(VideoLayer(), nil); // Present a non-video surface with DRM still set, so the video layer is torn // down by the layer type changing alone, without a DRM state change. ASSERT_NO_FATAL_FAILURE(Present(CreateBGRASurface(), PresentFlags::DRM)); ASSERT_EQ(VideoLayer(), nil); // Present a DRM video frame again; the rebuilt video layer must block // capture and hold no frame in its contents. ASSERT_NO_FATAL_FAILURE(Present(CreateNV12Surface(), PresentFlags::DRM)); ASSERT_NE(VideoLayer(), nil); EXPECT_TRUE(VideoLayer().preventsCapture); EXPECT_EQ(VideoLayer().contents, nil); } TEST_P(NativeLayerCAVideo, NonDRMVideoLayerShowsFirstFrame) { // Present a non-DRM HDR frame, which also uses a video layer; the layer // must show the first frame through its contents and allow capture. ASSERT_NO_FATAL_FAILURE(Present(CreateNV12Surface(), PresentFlags::HDR)); ASSERT_NE(VideoLayer(), nil); EXPECT_FALSE(VideoLayer().preventsCapture); EXPECT_NE(VideoLayer().contents, nil); } TEST_P(NativeLayerCAVideo, SwitchingToDRMDropsNonDRMFrame) { // Present a non-DRM HDR frame; the video layer shows it through its // contents. ASSERT_NO_FATAL_FAILURE(Present(CreateNV12Surface(), PresentFlags::HDR)); ASSERT_NE(VideoLayer(), nil); ASSERT_NE(VideoLayer().contents, nil); // Present a DRM HDR frame on the same layer; the video layer must block // capture and hold no frame in its contents. ASSERT_NO_FATAL_FAILURE( Present(CreateNV12Surface(), PresentFlags::DRM | PresentFlags::HDR)); ASSERT_NE(VideoLayer(), nil); EXPECT_TRUE(VideoLayer().preventsCapture); EXPECT_EQ(VideoLayer().contents, nil); } TEST_P(NativeLayerCAVideo, SwitchingFromDRMUsesNewLayer) { // Present a DRM HDR frame; the video layer blocks capture. Keep it alive to // inspect it after it is replaced. ASSERT_NO_FATAL_FAILURE( Present(CreateNV12Surface(), PresentFlags::DRM | PresentFlags::HDR)); AVSampleBufferDisplayLayer* drmLayer = [VideoLayer() retain]; auto releaseDRMLayer = MakeScopeExit([&] { [drmLayer release]; }); ASSERT_NE(drmLayer, nil); ASSERT_TRUE(drmLayer.preventsCapture); // Present a non-DRM HDR frame on the same layer; it must go to a new video // layer that shows its first frame, while the layer that held DRM frames // stays capture-protected. ASSERT_NO_FATAL_FAILURE(Present(CreateNV12Surface(), PresentFlags::HDR)); ASSERT_NE(VideoLayer(), nil); EXPECT_NE(VideoLayer(), drmLayer); EXPECT_FALSE(VideoLayer().preventsCapture); EXPECT_NE(VideoLayer().contents, nil); EXPECT_TRUE(drmLayer.preventsCapture); } INSTANTIATE_TEST_SUITE_P(, NativeLayerCAVideo, ::testing::Bool(), [](const ::testing::TestParamInfo& aInfo) { return aInfo.param ? "Offscreen" : "Onscreen"; });