diff --git a/Bundle/Linux/shaders b/Bundle/Linux/shaders index 6c668e8..f7f0c5e 100644 Binary files a/Bundle/Linux/shaders and b/Bundle/Linux/shaders differ diff --git a/Bundle/OSX/shaders b/Bundle/OSX/shaders index 6aeaeaa..e2a8e85 100644 Binary files a/Bundle/OSX/shaders and b/Bundle/OSX/shaders differ diff --git a/Bundle/Win64/shaders b/Bundle/Win64/shaders index 6503a4f..4b90720 100644 Binary files a/Bundle/Win64/shaders and b/Bundle/Win64/shaders differ diff --git a/Util/FFmpeg/FFmpegNativeDecoder.cs b/Util/FFmpeg/FFmpegNativeDecoder.cs index 46737b8..d85038c 100644 --- a/Util/FFmpeg/FFmpegNativeDecoder.cs +++ b/Util/FFmpeg/FFmpegNativeDecoder.cs @@ -206,6 +206,12 @@ internal sealed unsafe class FFmpegNativeDecoder : IDisposable private int _srcWidth; private int _srcHeight; + /// YUV→RGB matrix id (SWS_CS_*) applied to the context. 0 when no matrix is set (non-RGBA output). + private int _srcSwsColorspace; + + /// Whether the source range applied to the context is the full range. + private bool _srcFullRange; + // ────────────────────────────────────────────────────────────── // Public Properties // ────────────────────────────────────────────────────────────── @@ -296,6 +302,59 @@ internal sealed unsafe class FFmpegNativeDecoder : IDisposable } } + /// + /// Color matrix id used to convert the YUV frames of this stream to RGB, selected from the colorspace tag of the stream. + /// The ids are shared with the ColorMatrix uniform of ColorConversion.compute: + /// 0 = BT.709 (also used when the stream has no usable tag), 1 = BT.601, 2 = BT.2020 (non-constant luminance). + /// + public int ColorMatrix + { + get + { + lock (_lock) + { + if (_pCodecContext != null && _pCodecContext->colorspace != AVColorSpace.AVCOL_SPC_UNSPECIFIED) + { + return GetColorMatrixId(_pCodecContext->colorspace); + } + if (_pFormatContext != null && _videoStreamIndex >= 0) + { + return GetColorMatrixId(_pFormatContext->streams[_videoStreamIndex]->codecpar->color_space); + } + return ColorMatrixBT709; + } + } + } + + /// Color matrix id of BT.709. + // ReSharper disable InconsistentNaming + public const int ColorMatrixBT709 = 0; + + /// Color matrix id of BT.601 (SMPTE 170M / BT.470 BG). + public const int ColorMatrixBT601 = 1; + + /// Color matrix id of BT.2020 (non-constant luminance). + public const int ColorMatrixBT2020 = 2; + // ReSharper restore InconsistentNaming + + /// + /// Converts a colorspace tag to a color matrix id (see ). + /// Tags that have no matrix of their own (unspecified, RGB, other standards) use BT.709. + /// + private static int GetColorMatrixId(AVColorSpace colorspace) + { + switch (colorspace) + { + case AVColorSpace.AVCOL_SPC_SMPTE170M: + case AVColorSpace.AVCOL_SPC_BT470BG: + return ColorMatrixBT601; + case AVColorSpace.AVCOL_SPC_BT2020_NCL: + return ColorMatrixBT2020; + default: + return ColorMatrixBT709; + } + } + /// /// Gets a value indicating whether hardware-accelerated decoding is currently active. /// @@ -778,12 +837,67 @@ internal sealed unsafe class FFmpegNativeDecoder : IDisposable } /// - /// Lazily initializes or updates the when source/destination formats or dimensions change. + /// Selects the YUV→RGB conversion matrix (libswscale SWS_CS_*) from the colorspace tag of a frame. + /// The matrix set is the same as that of the compute shader path (ColorConversion.compute, ColorMatrix uniform): + /// BT.709 (also for frames without a usable tag), BT.601 and BT.2020, so both paths give the same colors. + /// Reference: https://ffmpeg.org/doxygen/trunk/group__libsws.html (sws_getCoefficients) + /// + private static int GetSwsColorspace(AVColorSpace colorspace) + { + switch (GetColorMatrixId(colorspace)) + { + case ColorMatrixBT601: + return ffmpeg.SWS_CS_ITU601; + case ColorMatrixBT2020: + return ffmpeg.SWS_CS_BT2020; + default: + return ffmpeg.SWS_CS_ITU709; + } + } + + /// + /// Returns true when the Y/U/V samples of the frame use the full range (0-255) instead of the limited range (16-235 / 16-240). + /// The range is given by the frame's color_range tag or by a deprecated full range pixel format (YUVJ*). + /// + private static bool IsFullRange(AVFrame* frame) + { + if (frame->color_range == AVColorRange.AVCOL_RANGE_JPEG) return true; + + switch ((AVPixelFormat)frame->format) + { + case AVPixelFormat.AV_PIX_FMT_YUVJ420P: + case AVPixelFormat.AV_PIX_FMT_YUVJ422P: + case AVPixelFormat.AV_PIX_FMT_YUVJ444P: + case AVPixelFormat.AV_PIX_FMT_YUVJ440P: + return true; + default: + return false; + } + } + + /// + /// Lazily initializes or updates the when source/destination formats, dimensions, + /// or (for RGBA output) the colorspace / range of the source frame change. + /// For RGBA output the YUV→RGB matrix and the source range are taken from . /// Reference: https://ffmpeg.org/doxygen/trunk/group__libsws.html /// - private void InitOrUpdateSwsContext(AVPixelFormat srcFormat, int srcWidth, int srcHeight, AVPixelFormat dstFormat) + /// Pixel format of the source frame. + /// Width of the source frame in pixels. + /// Height of the source frame in pixels. + /// Pixel format of the output. + /// Source frame whose colorspace / range tags are applied. Required for RGBA output. + private void InitOrUpdateSwsContext(AVPixelFormat srcFormat, int srcWidth, int srcHeight, AVPixelFormat dstFormat, AVFrame* colorFrame = null) { - if (_pSwsContext != null && _srcPixFmt == srcFormat && _srcWidth == srcWidth && _srcHeight == srcHeight && _dstPixFmt == dstFormat) + var swsColorspace = 0; + var fullRange = false; + if (colorFrame != null && dstFormat == AVPixelFormat.AV_PIX_FMT_RGBA) + { + swsColorspace = GetSwsColorspace(colorFrame->colorspace); + fullRange = IsFullRange(colorFrame); + } + + if (_pSwsContext != null && _srcPixFmt == srcFormat && _srcWidth == srcWidth && _srcHeight == srcHeight && _dstPixFmt == dstFormat + && _srcSwsColorspace == swsColorspace && _srcFullRange == fullRange) { return; } @@ -798,6 +912,8 @@ internal sealed unsafe class FFmpegNativeDecoder : IDisposable _srcWidth = srcWidth; _srcHeight = srcHeight; _dstPixFmt = dstFormat; + _srcSwsColorspace = swsColorspace; + _srcFullRange = fullRange; _pSwsContext = ffmpeg.sws_getContext( srcWidth, srcHeight, srcFormat, @@ -809,6 +925,44 @@ internal sealed unsafe class FFmpegNativeDecoder : IDisposable { throw new Exception($"Failed to initialize SwsContext (src: {srcFormat} {srcWidth}x{srcHeight} -> dst: {dstFormat} {Width}x{Height})."); } + + if (swsColorspace != 0) + { + ApplySwsColorspace(swsColorspace, fullRange); + } + } + + /// + /// Sets the YUV→RGB matrix and the source range of . + /// The destination range, brightness, contrast and saturation keep the values of the context. + /// Reference: https://ffmpeg.org/doxygen/trunk/group__libsws.html (sws_setColorspaceDetails) + /// + /// Matrix id (SWS_CS_*) of the source. + /// True when the source uses the full range. + private void ApplySwsColorspace(int swsColorspace, bool fullRange) + { + int* currentInvTable; + int* currentTable; + int srcRange, dstRange, brightness, contrast, saturation; + + // Returns a negative value when the context cannot change its colorspace details (e.g. YUV to YUV conversion). + if (ffmpeg.sws_getColorspaceDetails(_pSwsContext, ¤tInvTable, &srcRange, ¤tTable, &dstRange, + &brightness, &contrast, &saturation) < 0) + { + return; + } + + var coefficients = ffmpeg.sws_getCoefficients(swsColorspace); + var invTable = new int_array4(); + var table = new int_array4(); + for (uint i = 0; i < 4; i++) + { + invTable[i] = coefficients[i]; + table[i] = currentTable[i]; + } + + ffmpeg.sws_setColorspaceDetails(_pSwsContext, invTable, fullRange ? 1 : 0, table, dstRange, + brightness, contrast, saturation); } /// @@ -900,7 +1054,7 @@ internal sealed unsafe class FFmpegNativeDecoder : IDisposable if (frame == null) return false; timestamp = GetTimestampAndUpdateIndex(frame); - InitOrUpdateSwsContext((AVPixelFormat)frame->format, frame->width, frame->height, AVPixelFormat.AV_PIX_FMT_RGBA); + InitOrUpdateSwsContext((AVPixelFormat)frame->format, frame->width, frame->height, AVPixelFormat.AV_PIX_FMT_RGBA, frame); ClearConvertedFramePointers(); if (vflip) diff --git a/Util/VideoBGPlayer.cs b/Util/VideoBGPlayer.cs index 7df95e3..c27b8b4 100644 --- a/Util/VideoBGPlayer.cs +++ b/Util/VideoBGPlayer.cs @@ -20,6 +20,7 @@ internal class VideoBGPlayer : MonoBehaviour private static readonly int UTex = Shader.PropertyToID("_UTex"); private static readonly int VTex = Shader.PropertyToID("_VTex"); private static readonly int ColorRange = Shader.PropertyToID("ColorRange"); + private static readonly int ColorMatrix = Shader.PropertyToID("ColorMatrix"); [SuppressMessage("ReSharper", "InconsistentNaming")] private enum GPUMode @@ -307,6 +308,7 @@ internal class VideoBGPlayer : MonoBehaviour _computeShader.SetInt(Width, w); _computeShader.SetInt(Height, h); _computeShader.SetInt(ColorRange, _decoder.ColorRange == AVColorRange.AVCOL_RANGE_JPEG ? 1 : 0); + _computeShader.SetInt(ColorMatrix, _decoder.ColorMatrix); int threadGroupsX = (w + 7) / 8; int threadGroupsY = (h + 7) / 8; @@ -341,6 +343,7 @@ internal class VideoBGPlayer : MonoBehaviour _computeShader.SetInt(Width, w); _computeShader.SetInt(Height, h); _computeShader.SetInt(ColorRange, _decoder.ColorRange == AVColorRange.AVCOL_RANGE_JPEG ? 1 : 0); + _computeShader.SetInt(ColorMatrix, _decoder.ColorMatrix); int threadGroupsX = (w + 7) / 8; int threadGroupsY = (h + 7) / 8;