/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set ts=2 sw=2 sts=2 et cindent: */ /* 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 "mozilla/dom/VideoFrame.h" #include #include #include #include "ImageContainer.h" #include "ImageConversion.h" #include "MediaResult.h" #include "VideoColorSpace.h" #include "js/StructuredClone.h" #include "mozilla/Maybe.h" #include "mozilla/ResultVariant.h" #include "mozilla/ScopeExit.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/Try.h" #include "mozilla/UniquePtr.h" #include "mozilla/dom/BufferSourceBinding.h" #include "mozilla/dom/CanvasUtils.h" #include "mozilla/dom/DOMRect.h" #include "mozilla/dom/HTMLCanvasElement.h" #include "mozilla/dom/HTMLImageElement.h" #include "mozilla/dom/HTMLVideoElement.h" #include "mozilla/dom/ImageBitmap.h" #include "mozilla/dom/ImageUtils.h" #include "mozilla/dom/OffscreenCanvas.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/SVGImageElement.h" #include "mozilla/dom/StructuredCloneHolder.h" #include "mozilla/dom/StructuredCloneTags.h" #include "mozilla/dom/UnionTypes.h" #include "mozilla/dom/VideoFrameBinding.h" #include "mozilla/gfx/2D.h" #include "mozilla/gfx/Swizzle.h" #include "mozilla/layers/LayersSurfaces.h" #include "nsIPrincipal.h" #include "nsIURI.h" #include "nsLayoutUtils.h" extern mozilla::LazyLogModule gWebCodecsLog; namespace mozilla::dom { #ifdef LOG_INTERNAL # undef LOG_INTERNAL #endif // LOG_INTERNAL #define LOG_INTERNAL(level, msg, ...) \ MOZ_LOG(gWebCodecsLog, LogLevel::level, (msg, ##__VA_ARGS__)) #ifdef LOG # undef LOG #endif // LOG #define LOG(msg, ...) LOG_INTERNAL(Debug, msg, ##__VA_ARGS__) #ifdef LOGW # undef LOGW #endif // LOGW #define LOGW(msg, ...) LOG_INTERNAL(Warning, msg, ##__VA_ARGS__) #ifdef LOGE # undef LOGE #endif // LOGE #define LOGE(msg, ...) LOG_INTERNAL(Error, msg, ##__VA_ARGS__) NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(VideoFrame) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(VideoFrame) tmp->CloseIfNeeded(); NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent) NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(VideoFrame) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTING_ADDREF(VideoFrame) // VideoFrame should be released as soon as its refcount drops to zero, // without waiting for async deletion by the cycle collector, since it may hold // a large-size image. NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_LAST_RELEASE(VideoFrame, CloseIfNeeded()) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(VideoFrame) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END /* * The following are helpers to read the image data from the given buffer and * the format. The data layout is illustrated in the comments for * `VideoFrame::Format` below. */ static int32_t CeilingOfHalf(int32_t aValue) { MOZ_ASSERT(aValue >= 0); return aValue / 2 + (aValue % 2); } class YUVBufferReaderBase { public: YUVBufferReaderBase(const Span& aBuffer, int32_t aWidth, int32_t aHeight) : mWidth(aWidth), mHeight(aHeight), mStrideY(aWidth), mBuffer(aBuffer) {} virtual ~YUVBufferReaderBase() = default; const uint8_t* DataY() const { return mBuffer.data(); } const int32_t mWidth; const int32_t mHeight; const int32_t mStrideY; protected: CheckedInt YByteSize() const { return CheckedInt(mStrideY) * mHeight; } const Span mBuffer; }; class I420ABufferReader; class I420BufferReader : public YUVBufferReaderBase { public: I420BufferReader(const Span& aBuffer, int32_t aWidth, int32_t aHeight) : YUVBufferReaderBase(aBuffer, aWidth, aHeight), mStrideU(CeilingOfHalf(aWidth)), mStrideV(CeilingOfHalf(aWidth)) {} virtual ~I420BufferReader() = default; const uint8_t* DataU() const { return &mBuffer[YByteSize().value()]; } const uint8_t* DataV() const { return &mBuffer[YByteSize().value() + UByteSize().value()]; } virtual I420ABufferReader* AsI420ABufferReader() { return nullptr; } const int32_t mStrideU; const int32_t mStrideV; protected: CheckedInt UByteSize() const { return CheckedInt(CeilingOfHalf(mHeight)) * mStrideU; } CheckedInt VSize() const { return CheckedInt(CeilingOfHalf(mHeight)) * mStrideV; } }; class I420ABufferReader final : public I420BufferReader { public: I420ABufferReader(const Span& aBuffer, int32_t aWidth, int32_t aHeight) : I420BufferReader(aBuffer, aWidth, aHeight), mStrideA(aWidth) { MOZ_ASSERT(mStrideA == mStrideY); } virtual ~I420ABufferReader() = default; const uint8_t* DataA() const { return &mBuffer[YByteSize().value() + UByteSize().value() + VSize().value()]; } virtual I420ABufferReader* AsI420ABufferReader() override { return this; } const int32_t mStrideA; }; class NV12BufferReader final : public YUVBufferReaderBase { public: NV12BufferReader(const Span& aBuffer, int32_t aWidth, int32_t aHeight) : YUVBufferReaderBase(aBuffer, aWidth, aHeight), mStrideUV(aWidth + aWidth % 2) {} virtual ~NV12BufferReader() = default; const uint8_t* DataUV() const { return &mBuffer[YByteSize().value()]; } const int32_t mStrideUV; }; /* * The followings are helpers to create a VideoFrame from a given buffer */ static Result, MediaResult> AllocateBGRASurface( gfx::DataSourceSurface* aSurface) { MOZ_ASSERT(aSurface); // Memory allocation relies on CreateDataSourceSurfaceWithStride so we still // need to do this even if the format is SurfaceFormat::BGR{A, X}. gfx::DataSourceSurface::ScopedMap surfaceMap(aSurface, gfx::DataSourceSurface::READ); if (!surfaceMap.IsMapped()) { return Err(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "The source surface is not readable"_ns)); } RefPtr bgraSurface = gfx::Factory::CreateDataSourceSurfaceWithStride( aSurface->GetSize(), gfx::SurfaceFormat::B8G8R8A8, surfaceMap.GetStride()); if (!bgraSurface) { return Err(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "Failed to allocate a BGRA surface"_ns)); } gfx::DataSourceSurface::ScopedMap bgraMap(bgraSurface, gfx::DataSourceSurface::WRITE); if (!bgraMap.IsMapped()) { return Err(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "The allocated BGRA surface is not writable"_ns)); } gfx::SwizzleData(surfaceMap.GetData(), surfaceMap.GetStride(), aSurface->GetFormat(), bgraMap.GetData(), bgraMap.GetStride(), bgraSurface->GetFormat(), bgraSurface->GetSize()); return bgraSurface; } static Result, MediaResult> CreateImageFromSourceSurface( gfx::SourceSurface* aSource) { MOZ_ASSERT(aSource); if (aSource->GetSize().IsEmpty()) { return Err(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "Surface has non positive width or height"_ns)); } RefPtr surface = aSource->GetDataSurface(); if (!surface) { return Err(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "Failed to get the data surface"_ns)); } // Gecko favors BGRA so we convert surface into BGRA format first. RefPtr bgraSurface; MOZ_TRY_VAR(bgraSurface, AllocateBGRASurface(surface)); return RefPtr( new layers::SourceSurfaceImage(bgraSurface.get())); } static Result, MediaResult> CreateImageFromRawData( const gfx::IntSize& aSize, int32_t aStride, gfx::SurfaceFormat aFormat, const Span& aBuffer) { MOZ_ASSERT(!aSize.IsEmpty()); // Wrap the source buffer into a DataSourceSurface. RefPtr surface = gfx::Factory::CreateWrappingDataSourceSurface(aBuffer.data(), aStride, aSize, aFormat); if (!surface) { return Err(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "Failed to wrap the raw data into a surface"_ns)); } // Gecko favors BGRA so we convert surface into BGRA format first. RefPtr bgraSurface; MOZ_TRY_VAR(bgraSurface, AllocateBGRASurface(surface)); MOZ_ASSERT(bgraSurface); return RefPtr( new layers::SourceSurfaceImage(bgraSurface.get())); } static Result, MediaResult> CreateRGBAImageFromBuffer( const VideoFrame::Format& aFormat, const gfx::IntSize& aSize, const Span& aBuffer) { const gfx::SurfaceFormat format = aFormat.ToSurfaceFormat(); MOZ_ASSERT(format == gfx::SurfaceFormat::R8G8B8A8 || format == gfx::SurfaceFormat::R8G8B8X8 || format == gfx::SurfaceFormat::B8G8R8A8 || format == gfx::SurfaceFormat::B8G8R8X8); // TODO: Use aFormat.SampleBytes() instead? CheckedInt stride(BytesPerPixel(format)); stride *= aSize.Width(); if (!stride.isValid()) { return Err(MediaResult(NS_ERROR_INVALID_ARG, "Image size exceeds implementation's limit"_ns)); } return CreateImageFromRawData(aSize, stride.value(), format, aBuffer); } static Result, MediaResult> CreateYUVImageFromBuffer( const VideoFrame::Format& aFormat, const VideoColorSpaceInternal& aColorSpace, const gfx::IntSize& aSize, const Span& aBuffer) { if (aFormat.PixelFormat() == VideoPixelFormat::I420 || aFormat.PixelFormat() == VideoPixelFormat::I420A) { UniquePtr reader; if (aFormat.PixelFormat() == VideoPixelFormat::I420) { reader.reset( new I420BufferReader(aBuffer, aSize.Width(), aSize.Height())); } else { reader.reset( new I420ABufferReader(aBuffer, aSize.Width(), aSize.Height())); } layers::PlanarYCbCrData data; data.mPictureRect = gfx::IntRect(0, 0, reader->mWidth, reader->mHeight); // Y plane. data.mYChannel = const_cast(reader->DataY()); data.mYStride = reader->mStrideY; data.mYSkip = 0; // Cb plane. data.mCbChannel = const_cast(reader->DataU()); data.mCbSkip = 0; // Cr plane. data.mCrChannel = const_cast(reader->DataV()); data.mCbSkip = 0; // A plane. if (aFormat.PixelFormat() == VideoPixelFormat::I420A) { data.mAlpha.emplace(); data.mAlpha->mChannel = const_cast(reader->AsI420ABufferReader()->DataA()); data.mAlpha->mSize = data.mPictureRect.Size(); // No values for mDepth and mPremultiplied. } // CbCr plane vector. MOZ_RELEASE_ASSERT(reader->mStrideU == reader->mStrideV); data.mCbCrStride = reader->mStrideU; data.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT; // Color settings. if (aColorSpace.mFullRange) { data.mColorRange = ToColorRange(aColorSpace.mFullRange.value()); } MOZ_RELEASE_ASSERT(aColorSpace.mMatrix); data.mYUVColorSpace = ToColorSpace(aColorSpace.mMatrix.value()); if (aColorSpace.mTransfer) { data.mTransferFunction = ToTransferFunction(aColorSpace.mTransfer.value()); } if (aColorSpace.mPrimaries) { data.mColorPrimaries = ToPrimaries(aColorSpace.mPrimaries.value()); } RefPtr image = new layers::RecyclingPlanarYCbCrImage(new layers::BufferRecycleBin()); nsresult r = image->CopyData(data); if (NS_FAILED(r)) { return Err(MediaResult( r, nsPrintfCString( "Failed to create I420%s image", (aFormat.PixelFormat() == VideoPixelFormat::I420A ? "A" : "")))); } // Manually cast type to make Result work. return RefPtr(image.forget()); } if (aFormat.PixelFormat() == VideoPixelFormat::NV12) { NV12BufferReader reader(aBuffer, aSize.Width(), aSize.Height()); layers::PlanarYCbCrData data; data.mPictureRect = gfx::IntRect(0, 0, reader.mWidth, reader.mHeight); // Y plane. data.mYChannel = const_cast(reader.DataY()); data.mYStride = reader.mStrideY; data.mYSkip = 0; // Cb plane. data.mCbChannel = const_cast(reader.DataUV()); data.mCbSkip = 1; // Cr plane. data.mCrChannel = data.mCbChannel + 1; data.mCrSkip = 1; // CbCr plane vector. data.mCbCrStride = reader.mStrideUV; data.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT; // Color settings. if (aColorSpace.mFullRange) { data.mColorRange = ToColorRange(aColorSpace.mFullRange.value()); } MOZ_RELEASE_ASSERT(aColorSpace.mMatrix); data.mYUVColorSpace = ToColorSpace(aColorSpace.mMatrix.value()); if (aColorSpace.mTransfer) { data.mTransferFunction = ToTransferFunction(aColorSpace.mTransfer.value()); } if (aColorSpace.mPrimaries) { data.mColorPrimaries = ToPrimaries(aColorSpace.mPrimaries.value()); } RefPtr image = new layers::NVImage(); nsresult r = image->SetData(data); if (NS_FAILED(r)) { return Err(MediaResult(r, "Failed to create NV12 image"_ns)); } // Manually cast type to make Result work. return RefPtr(image.forget()); } return Err(MediaResult( NS_ERROR_DOM_NOT_SUPPORTED_ERR, nsPrintfCString("%s is unsupported", dom::GetEnumString(aFormat.PixelFormat()).get()))); } static Result, MediaResult> CreateImageFromBuffer( const VideoFrame::Format& aFormat, const VideoColorSpaceInternal& aColorSpace, const gfx::IntSize& aSize, const Span& aBuffer) { switch (aFormat.PixelFormat()) { case VideoPixelFormat::I420: case VideoPixelFormat::I420A: case VideoPixelFormat::NV12: return CreateYUVImageFromBuffer(aFormat, aColorSpace, aSize, aBuffer); case VideoPixelFormat::I420P10: case VideoPixelFormat::I420P12: case VideoPixelFormat::I420AP10: case VideoPixelFormat::I420AP12: case VideoPixelFormat::I422: case VideoPixelFormat::I422P10: case VideoPixelFormat::I422P12: case VideoPixelFormat::I422A: case VideoPixelFormat::I422AP10: case VideoPixelFormat::I422AP12: case VideoPixelFormat::I444: case VideoPixelFormat::I444P10: case VideoPixelFormat::I444P12: case VideoPixelFormat::I444A: case VideoPixelFormat::I444AP10: case VideoPixelFormat::I444AP12: // Not yet support for now. break; case VideoPixelFormat::RGBA: case VideoPixelFormat::RGBX: case VideoPixelFormat::BGRA: case VideoPixelFormat::BGRX: return CreateRGBAImageFromBuffer(aFormat, aSize, aBuffer); } return Err(MediaResult( NS_ERROR_DOM_NOT_SUPPORTED_ERR, nsPrintfCString("%s is unsupported", dom::GetEnumString(aFormat.PixelFormat()).get()))); } /* * The followings are helpers defined in * https://w3c.github.io/webcodecs/#videoframe-algorithms */ static bool IsSameOrigin(nsIGlobalObject* aGlobal, const VideoFrame& aFrame) { MOZ_ASSERT(aGlobal); MOZ_ASSERT(aFrame.GetParentObject()); nsIPrincipal* principalX = aGlobal->PrincipalOrNull(); nsIPrincipal* principalY = aFrame.GetParentObject()->PrincipalOrNull(); // If both of VideoFrames are created in worker, they are in the same origin // domain. if (!principalX) { return !principalY; } // Otherwise, check their domains. return principalX->Equals(principalY); } static bool IsSameOrigin(nsIGlobalObject* aGlobal, HTMLVideoElement& aVideoElement) { MOZ_ASSERT(aGlobal); // If CORS is in use, consider the video source is same-origin. if (aVideoElement.GetCORSMode() != CORS_NONE) { return true; } // Otherwise, check if video source has cross-origin redirect or not. if (aVideoElement.HadCrossOriginRedirects()) { return false; } // Finally, compare the VideoFrame's domain and video's one. nsIPrincipal* principal = aGlobal->PrincipalOrNull(); nsCOMPtr elementPrincipal = aVideoElement.GetCurrentVideoPrincipal(); //