From bf654bde600fa6a67b19d27cf82f600a47ca3638 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Sat, 30 Mar 2024 05:55:15 +0800 Subject: [PATCH 1/6] Add json parser for video rotation side data Signed-off-by: nyanmisaka --- .../Probing/MediaStreamInfoSideData.cs | 7 +++++++ .../Probing/ProbeResultNormalizer.cs | 6 +++++- MediaBrowser.Model/Entities/MediaStream.cs | 6 ++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfoSideData.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfoSideData.cs index 5dbc438e4..65d1ddf89 100644 --- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfoSideData.cs +++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfoSideData.cs @@ -69,5 +69,12 @@ namespace MediaBrowser.MediaEncoding.Probing /// The DvBlSignalCompatibilityId. [JsonPropertyName("dv_bl_signal_compatibility_id")] public int? DvBlSignalCompatibilityId { get; set; } + + /// + /// Gets or sets the Rotation. + /// + /// The Rotation. + [JsonPropertyName("rotation")] + public int? Rotation { get; set; } } } diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 5397a6752..0a42d504a 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -887,8 +887,12 @@ namespace MediaBrowser.MediaEncoding.Probing stream.ElPresentFlag = data.ElPresentFlag; stream.BlPresentFlag = data.BlPresentFlag; stream.DvBlSignalCompatibilityId = data.DvBlSignalCompatibilityId; + } - break; + // Parse video rotation metadata from side_data + else if (string.Equals(data.SideDataType, "Display Matrix", StringComparison.OrdinalIgnoreCase)) + { + stream.Rotation = data.Rotation; } } } diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index a620bc9b5..fa09902e5 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -122,6 +122,12 @@ namespace MediaBrowser.Model.Entities /// The Dolby Vision bl signal compatibility id. public int? DvBlSignalCompatibilityId { get; set; } + /// + /// Gets or sets the Rotation. + /// + /// The video rotation. + public int? Rotation { get; set; } + /// /// Gets or sets the comment. /// From 5967d26228e99dede275d5807a583e7811634e37 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Sat, 30 Mar 2024 04:36:45 +0800 Subject: [PATCH 2/6] Register video rotation side data in DB Signed-off-by: nyanmisaka --- .../Data/SqliteItemRepository.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 59e4ff1a9..f1e60915d 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -182,7 +182,8 @@ namespace Emby.Server.Implementations.Data "ElPresentFlag", "BlPresentFlag", "DvBlSignalCompatibilityId", - "IsHearingImpaired" + "IsHearingImpaired", + "Rotation" }; private static readonly string _mediaStreamSaveColumnsInsertQuery = @@ -343,7 +344,7 @@ namespace Emby.Server.Implementations.Data base.Initialize(); const string CreateMediaStreamsTableCommand - = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, IsHearingImpaired BIT NULL, PRIMARY KEY (ItemId, StreamIndex))"; + = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, IsHearingImpaired BIT NULL, Rotation INT NULL, PRIMARY KEY (ItemId, StreamIndex))"; const string CreateMediaAttachmentsTableCommand = "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))"; @@ -537,6 +538,8 @@ namespace Emby.Server.Implementations.Data AddColumn(connection, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames); + AddColumn(connection, "MediaStreams", "Rotation", "INT", existingColumnNames); + connection.Execute(string.Join(';', postQueries)); transaction.Commit(); @@ -5479,6 +5482,8 @@ AND Type = @InternalPersonType)"); statement.TryBind("@DvBlSignalCompatibilityId" + index, stream.DvBlSignalCompatibilityId); statement.TryBind("@IsHearingImpaired" + index, stream.IsHearingImpaired); + + statement.TryBind("@Rotation" + index, stream.Rotation); } statement.ExecuteNonQuery(); @@ -5690,6 +5695,11 @@ AND Type = @InternalPersonType)"); item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result; + if (reader.TryGetInt32(44, out var rotation)) + { + item.Rotation = rotation; + } + if (item.Type == MediaStreamType.Subtitle) { item.LocalizedUndefined = _localization.GetLocalizedString("Undefined"); From 6076462ff58f7901d1744516cbe30fe448b3e017 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Sat, 30 Mar 2024 04:37:01 +0800 Subject: [PATCH 3/6] Add tests for the video rotation side data Signed-off-by: nyanmisaka --- .../Probing/ProbeResultNormalizerTests.cs | 1 + .../Test Data/Probing/video_metadata.json | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 020e20fb8..1becf07f5 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -84,6 +84,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Assert.Equal(0, res.VideoStream.ElPresentFlag); Assert.Equal(1, res.VideoStream.BlPresentFlag); Assert.Equal(0, res.VideoStream.DvBlSignalCompatibilityId); + Assert.Equal(-180, res.VideoStream.Rotation); var audio1 = res.MediaStreams[1]; Assert.Equal("eac3", audio1.Codec); diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json index a49c68690..df41ab16e 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json +++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json @@ -59,6 +59,10 @@ "el_present_flag": 0, "bl_present_flag": 1, "dv_bl_signal_compatibility_id": 0 + }, + { + "side_data_type": "Display Matrix", + "rotation": -180 } ] }, From 95b81ff54a9894d40fc54a30ce6edc0f899d59aa Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Sat, 30 Mar 2024 04:27:27 +0800 Subject: [PATCH 4/6] Only streamCopy videos containing rotation data in fMP4 Signed-off-by: nyanmisaka --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index f8d89119a..e5e4356f8 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -190,6 +190,17 @@ public class DynamicHlsHelper AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User); } + // Video rotation metadata is only supported in fMP4 remuxing + if (state.VideoStream is not null + && state.VideoRequest is not null + && (state.VideoStream?.Rotation ?? 0) != 0 + && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer) + && !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase)) + { + playlistUrl += "&AllowVideoStreamCopy=false"; + } + var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); if (state.VideoStream is not null && state.VideoRequest is not null) From deb36eeedaba2f1421b92d290d85d45bfe48d1f5 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Sat, 30 Mar 2024 06:09:44 +0800 Subject: [PATCH 5/6] Add HWA video transpose support (+-90/180) Signed-off-by: nyanmisaka --- .../MediaEncoding/EncodingHelper.cs | 360 +++++++++++++----- .../MediaEncoding/FilterOptionType.cs | 7 +- .../Encoder/EncoderValidator.cs | 9 +- 3 files changed, 276 insertions(+), 100 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 85963e66c..5143d5f74 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -62,6 +62,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1); private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0); private readonly Version _minFFmpegReadrateOption = new Version(5, 0); + private readonly Version _minFFmpegDisplayRotationOption = new Version(6, 0); private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled); @@ -229,6 +230,7 @@ namespace MediaBrowser.Controller.MediaEncoding && _mediaEncoder.SupportsFilter("tonemap_vaapi") && _mediaEncoder.SupportsFilter("procamp_vaapi") && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayVaapiFrameSync) + && _mediaEncoder.SupportsFilter("transpose_vaapi") && _mediaEncoder.SupportsFilter("hwupload_vaapi"); } @@ -246,6 +248,8 @@ namespace MediaBrowser.Controller.MediaEncoding && _mediaEncoder.SupportsFilter("scale_opencl") && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.TonemapOpenclBt2390) && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayOpenclFrameSync); + + // Let transpose_opencl optional for the time being. } private bool IsCudaFullSupported() @@ -256,6 +260,8 @@ namespace MediaBrowser.Controller.MediaEncoding && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.TonemapCudaName) && _mediaEncoder.SupportsFilter("overlay_cuda") && _mediaEncoder.SupportsFilter("hwupload_cuda"); + + // Let transpose_cuda optional for the time being. } private bool IsVulkanFullSupported() @@ -263,7 +269,9 @@ namespace MediaBrowser.Controller.MediaEncoding return _mediaEncoder.SupportsHwaccel("vulkan") && _mediaEncoder.SupportsFilter("libplacebo") && _mediaEncoder.SupportsFilter("scale_vulkan") - && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayVulkanFrameSync); + && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayVulkanFrameSync) + && _mediaEncoder.SupportsFilter("transpose_vulkan") + && _mediaEncoder.SupportsFilter("flip_vulkan"); } private bool IsVideoToolboxFullSupported() @@ -273,6 +281,8 @@ namespace MediaBrowser.Controller.MediaEncoding && _mediaEncoder.SupportsFilter("overlay_videotoolbox") && _mediaEncoder.SupportsFilter("tonemap_videotoolbox") && _mediaEncoder.SupportsFilter("scale_vt"); + + // Let transpose_vt optional for the time being. } private bool IsHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options) @@ -1156,9 +1166,6 @@ namespace MediaBrowser.Controller.MediaEncoding args.Append(vidDecoder); } - // hw transpose filters should be added manually. - args.Append(" -noautorotate"); - return args.ToString().Trim(); } @@ -3016,8 +3023,10 @@ namespace MediaBrowser.Controller.MediaEncoding } public static string GetHwScaleFilter( + string hwScalePrefix, string hwScaleSuffix, string videoFormat, + bool swapOutputWandH, int? videoWidth, int? videoHeight, int? requestedWidth, @@ -3039,8 +3048,11 @@ namespace MediaBrowser.Controller.MediaEncoding || !videoHeight.HasValue || outHeight.Value != videoHeight.Value; - var arg1 = isSizeFixed ? ("=w=" + outWidth.Value + ":h=" + outHeight.Value) : string.Empty; - var arg2 = isFormatFixed ? ("format=" + videoFormat) : string.Empty; + var swpOutW = swapOutputWandH ? outHeight.Value : outWidth.Value; + var swpOutH = swapOutputWandH ? outWidth.Value : outHeight.Value; + + var arg1 = isSizeFixed ? $"=w={swpOutW}:h={swpOutH}" : string.Empty; + var arg2 = isFormatFixed ? $"format={videoFormat}" : string.Empty; if (isFormatFixed) { arg2 = (isSizeFixed ? ':' : '=') + arg2; @@ -3050,7 +3062,8 @@ namespace MediaBrowser.Controller.MediaEncoding { return string.Format( CultureInfo.InvariantCulture, - "scale_{0}{1}{2}", + "{0}_{1}{2}{3}", + hwScalePrefix ?? "scale", hwScaleSuffix, arg1, arg2); @@ -3446,6 +3459,18 @@ namespace MediaBrowser.Controller.MediaEncoding tonemapArg); } + public string GetVideoTransposeDirection(EncodingJobInfo state) + { + return (state.VideoStream?.Rotation ?? 0) switch + { + 90 => "cclock", + 180 => "reversal", + -90 => "clock", + -180 => "reversal", + _ => string.Empty + }; + } + /// /// Gets the parameter of software filter chain. /// @@ -3479,6 +3504,11 @@ namespace MediaBrowser.Controller.MediaEncoding var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; + var rotation = state.VideoStream?.Rotation ?? 0; + var swapWAndH = Math.Abs(rotation) == 90; + var swpInW = swapWAndH ? inH : inW; + var swpInH = swapWAndH ? inW : inH; + /* Make main filters for video stream */ var mainFilters = new List(); @@ -3493,7 +3523,7 @@ namespace MediaBrowser.Controller.MediaEncoding } var outFormat = isSwDecoder ? "yuv420p" : "nv12"; - var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); if (isVaapiEncoder) { outFormat = "nv12"; @@ -3522,7 +3552,7 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } @@ -3596,6 +3626,13 @@ namespace MediaBrowser.Controller.MediaEncoding && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + var rotation = state.VideoStream?.Rotation ?? 0; + var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doCuTranspose = !string.IsNullOrEmpty(tranposeDir) && _mediaEncoder.SupportsFilter("transpose_cuda"); + var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isNvDecoder && doCuTranspose)); + var swpInW = swapWAndH ? inH : inW; + var swpInH = swapWAndH ? inW : inH; + /* Make main filters for video stream */ var mainFilters = new List(); @@ -3612,10 +3649,10 @@ namespace MediaBrowser.Controller.MediaEncoding } var outFormat = doCuTonemap ? "yuv420p10le" : "yuv420p"; - var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); // sw scale mainFilters.Add(swScaleFilter); - mainFilters.Add("format=" + outFormat); + mainFilters.Add($"format={outFormat}"); // sw => hw if (doCuTonemap) @@ -3634,8 +3671,14 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add(deintFilter); } + // hw transpose + if (doCuTranspose) + { + mainFilters.Add($"transpose_cuda=dir={tranposeDir}"); + } + var outFormat = doCuTonemap ? string.Empty : "yuv420p"; - var hwScaleFilter = GetHwScaleFilter("cuda", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var hwScaleFilter = GetHwScaleFilter("scale", "cuda", outFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); // hw scale mainFilters.Add(hwScaleFilter); } @@ -3685,7 +3728,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); subFilters.Add("format=yuva420p"); } @@ -3695,7 +3738,7 @@ namespace MediaBrowser.Controller.MediaEncoding var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10; // alphasrc=s=1280x720:r=10:start=0,format=yuva420p,subtitles,hwupload - var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate); + var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH, subFramerate); var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true); subFilters.Add(alphaSrcFilter); subFilters.Add("format=yuva420p"); @@ -3710,7 +3753,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } @@ -3786,6 +3829,14 @@ namespace MediaBrowser.Controller.MediaEncoding && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + var rotation = state.VideoStream?.Rotation ?? 0; + var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doOclTranspose = !string.IsNullOrEmpty(tranposeDir) + && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.TransposeOpenclReversal); + var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isD3d11vaDecoder && doOclTranspose)); + var swpInW = swapWAndH ? inH : inW; + var swpInH = swapWAndH ? inW : inH; + /* Make main filters for video stream */ var mainFilters = new List(); @@ -3802,10 +3853,10 @@ namespace MediaBrowser.Controller.MediaEncoding } var outFormat = doOclTonemap ? "yuv420p10le" : "yuv420p"; - var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); // sw scale mainFilters.Add(swScaleFilter); - mainFilters.Add("format=" + outFormat); + mainFilters.Add($"format={outFormat}"); // keep video at memory except ocl tonemap, // since the overhead caused by hwupload >>> using sw filter. @@ -3814,7 +3865,7 @@ namespace MediaBrowser.Controller.MediaEncoding { mainFilters.Add("hwupload=derive_device=d3d11va:extra_hw_frames=16"); mainFilters.Add("format=d3d11"); - mainFilters.Add("hwmap=derive_device=opencl"); + mainFilters.Add("hwmap=derive_device=opencl:mode=read"); } } @@ -3822,12 +3873,18 @@ namespace MediaBrowser.Controller.MediaEncoding { // INPUT d3d11 surface(vram) // map from d3d11va to opencl via d3d11-opencl interop. - mainFilters.Add("hwmap=derive_device=opencl"); + mainFilters.Add("hwmap=derive_device=opencl:mode=read"); // hw deint <= TODO: finsh the 'yadif_opencl' filter + // hw transpose + if (doOclTranspose) + { + mainFilters.Add($"transpose_opencl=dir={tranposeDir}"); + } + var outFormat = doOclTonemap ? string.Empty : "nv12"; - var hwScaleFilter = GetHwScaleFilter("opencl", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var hwScaleFilter = GetHwScaleFilter("scale", "opencl", outFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); // hw scale mainFilters.Add(hwScaleFilter); } @@ -3872,7 +3929,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // OUTPUT d3d11(nv12) surface(vram) // reverse-mapping via d3d11-opencl interop. - mainFilters.Add("hwmap=derive_device=d3d11va:reverse=1"); + mainFilters.Add("hwmap=derive_device=d3d11va:mode=write:reverse=1"); mainFilters.Add("format=d3d11"); } @@ -3885,7 +3942,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); subFilters.Add("format=yuva420p"); } @@ -3895,7 +3952,7 @@ namespace MediaBrowser.Controller.MediaEncoding var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10; // alphasrc=s=1280x720:r=10:start=0,format=yuva420p,subtitles,hwupload - var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate); + var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH, subFramerate); var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true); subFilters.Add(alphaSrcFilter); subFilters.Add("format=yuva420p"); @@ -3904,7 +3961,7 @@ namespace MediaBrowser.Controller.MediaEncoding subFilters.Add("hwupload=derive_device=opencl"); overlayFilters.Add("overlay_opencl=eof_action=pass:repeatlast=0"); - overlayFilters.Add("hwmap=derive_device=d3d11va:reverse=1"); + overlayFilters.Add("hwmap=derive_device=d3d11va:mode=write:reverse=1"); overlayFilters.Add("format=d3d11"); } } @@ -3912,7 +3969,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } @@ -4008,6 +4065,13 @@ namespace MediaBrowser.Controller.MediaEncoding && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + var rotation = state.VideoStream?.Rotation ?? 0; + var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVppTranspose = !string.IsNullOrEmpty(tranposeDir); + var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || ((isD3d11vaDecoder || isQsvDecoder) && doVppTranspose)); + var swpInW = swapWAndH ? inH : inW; + var swpInH = swapWAndH ? inW : inH; + /* Make main filters for video stream */ var mainFilters = new List(); @@ -4024,10 +4088,10 @@ namespace MediaBrowser.Controller.MediaEncoding } var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12"); - var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); // sw scale mainFilters.Add(swScaleFilter); - mainFilters.Add("format=" + outFormat); + mainFilters.Add($"format={outFormat}"); // keep video at memory except ocl tonemap, // since the overhead caused by hwupload >>> using sw filter. @@ -4039,8 +4103,15 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (isD3d11vaDecoder || isQsvDecoder) { - var outFormat = doOclTonemap ? string.Empty : "nv12"; - var hwScaleFilter = GetHwScaleFilter("qsv", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var outFormat = doOclTonemap ? (doVppTranspose ? "p010" : string.Empty) : "nv12"; + var swapOutputWandH = doVppTranspose && swapWAndH; + var hwScalePrefix = doVppTranspose ? "vpp" : "scale"; + var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, "qsv", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + + if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTranspose) + { + hwScaleFilter += $":transpose={tranposeDir}"; + } if (isD3d11vaDecoder) { @@ -4059,14 +4130,14 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add(deintFilter); } - // hw scale + // hw transpose & scale mainFilters.Add(hwScaleFilter); } if (doOclTonemap && isHwDecoder) { // map from qsv to opencl via qsv(d3d11)-opencl interop. - mainFilters.Add("hwmap=derive_device=opencl"); + mainFilters.Add("hwmap=derive_device=opencl:mode=read"); } // hw tonemap @@ -4110,7 +4181,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // OUTPUT qsv(nv12) surface(vram) // reverse-mapping via qsv(d3d11)-opencl interop. - mainFilters.Add("hwmap=derive_device=qsv:reverse=1"); + mainFilters.Add("hwmap=derive_device=qsv:mode=write:reverse=1"); mainFilters.Add("format=qsv"); } @@ -4124,7 +4195,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (hasGraphicalSubs) { // overlay_qsv can handle overlay scaling, setup a smaller height to reduce transfer overhead - var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, 1080); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, 1080); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -4134,7 +4205,7 @@ namespace MediaBrowser.Controller.MediaEncoding var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10; // alphasrc=s=1280x720:r=10:start=0,format=bgra,subtitles,hwupload - var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, 1080, subFramerate); + var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, 1080, subFramerate); var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true); subFilters.Add(alphaSrcFilter); subFilters.Add("format=bgra"); @@ -4145,9 +4216,9 @@ namespace MediaBrowser.Controller.MediaEncoding // default to 64 otherwise it will fail on certain iGPU. subFilters.Add("hwupload=derive_device=qsv:extra_hw_frames=64"); - var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var (overlayW, overlayH) = GetFixedOutputSize(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); var overlaySize = (overlayW.HasValue && overlayH.HasValue) - ? (":w=" + overlayW.Value + ":h=" + overlayH.Value) + ? $":w={overlayW.Value}:h={overlayH.Value}" : string.Empty; var overlayQsvFilter = string.Format( CultureInfo.InvariantCulture, @@ -4160,7 +4231,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } @@ -4205,6 +4276,13 @@ namespace MediaBrowser.Controller.MediaEncoding && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + var rotation = state.VideoStream?.Rotation ?? 0; + var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVppTranspose = !string.IsNullOrEmpty(tranposeDir); + var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || ((isVaapiDecoder || isQsvDecoder) && doVppTranspose)); + var swpInW = swapWAndH ? inH : inW; + var swpInH = swapWAndH ? inW : inH; + /* Make main filters for video stream */ var mainFilters = new List(); @@ -4221,10 +4299,10 @@ namespace MediaBrowser.Controller.MediaEncoding } var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12"); - var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); // sw scale mainFilters.Add(swScaleFilter); - mainFilters.Add("format=" + outFormat); + mainFilters.Add($"format={outFormat}"); // keep video at memory except ocl tonemap, // since the overhead caused by hwupload >>> using sw filter. @@ -4236,24 +4314,39 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (isVaapiDecoder || isQsvDecoder) { + var hwFilterSuffix = isVaapiDecoder ? "vaapi" : "qsv"; + // INPUT vaapi/qsv surface(vram) // hw deint if (doDeintH2645) { - var deintFilter = GetHwDeinterlaceFilter(state, options, isVaapiDecoder ? "vaapi" : "qsv"); + var deintFilter = GetHwDeinterlaceFilter(state, options, hwFilterSuffix); mainFilters.Add(deintFilter); } - var outFormat = doTonemap ? string.Empty : "nv12"; - var hwScaleFilter = GetHwScaleFilter(isVaapiDecoder ? "vaapi" : "qsv", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + // hw transpose(vaapi vpp) + if (isVaapiDecoder && doVppTranspose) + { + mainFilters.Add($"transpose_vaapi=dir={tranposeDir}"); + } - // allocate extra pool sizes for vaapi vpp + var outFormat = doOclTonemap ? ((isQsvDecoder && doVppTranspose) ? "p010" : string.Empty) : "nv12"; + var swapOutputWandH = isQsvDecoder && doVppTranspose && swapWAndH; + var hwScalePrefix = (isQsvDecoder && doVppTranspose) ? "vpp" : "scale"; + var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, hwFilterSuffix, outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + + if (!string.IsNullOrEmpty(hwScaleFilter) && isQsvDecoder && doVppTranspose) + { + hwScaleFilter += $":transpose={tranposeDir}"; + } + + // allocate extra pool sizes for vaapi vpp scale if (!string.IsNullOrEmpty(hwScaleFilter) && isVaapiDecoder) { hwScaleFilter += ":extra_hw_frames=24"; } - // hw scale + // hw transpose(qsv vpp) & scale mainFilters.Add(hwScaleFilter); } @@ -4279,7 +4372,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (doOclTonemap && isHwDecoder) { // map from qsv to opencl via qsv(vaapi)-opencl interop. - mainFilters.Add("hwmap=derive_device=opencl"); + mainFilters.Add("hwmap=derive_device=opencl:mode=read"); } // ocl tonemap @@ -4326,7 +4419,7 @@ namespace MediaBrowser.Controller.MediaEncoding // OUTPUT qsv(nv12) surface(vram) // reverse-mapping via qsv(vaapi)-opencl interop. // add extra pool size to avoid the 'cannot allocate memory' error on hevc_qsv. - mainFilters.Add("hwmap=derive_device=qsv:reverse=1:extra_hw_frames=16"); + mainFilters.Add("hwmap=derive_device=qsv:mode=write:reverse=1:extra_hw_frames=16"); mainFilters.Add("format=qsv"); } else if (isVaapiDecoder) @@ -4346,7 +4439,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (hasGraphicalSubs) { // overlay_qsv can handle overlay scaling, setup a smaller height to reduce transfer overhead - var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, 1080); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, 1080); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -4355,7 +4448,7 @@ namespace MediaBrowser.Controller.MediaEncoding var framerate = state.VideoStream?.RealFrameRate; var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10; - var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, 1080, subFramerate); + var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, 1080, subFramerate); var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true); subFilters.Add(alphaSrcFilter); subFilters.Add("format=bgra"); @@ -4366,9 +4459,9 @@ namespace MediaBrowser.Controller.MediaEncoding // default to 64 otherwise it will fail on certain iGPU. subFilters.Add("hwupload=derive_device=qsv:extra_hw_frames=64"); - var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var (overlayW, overlayH) = GetFixedOutputSize(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); var overlaySize = (overlayW.HasValue && overlayH.HasValue) - ? (":w=" + overlayW.Value + ":h=" + overlayH.Value) + ? $":w={overlayW.Value}:h={overlayH.Value}" : string.Empty; var overlayQsvFilter = string.Format( CultureInfo.InvariantCulture, @@ -4381,7 +4474,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } @@ -4491,6 +4584,13 @@ namespace MediaBrowser.Controller.MediaEncoding && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + var rotation = state.VideoStream?.Rotation ?? 0; + var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVaVppTranspose = !string.IsNullOrEmpty(tranposeDir); + var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isVaapiDecoder && doVaVppTranspose)); + var swpInW = swapWAndH ? inH : inW; + var swpInH = swapWAndH ? inW : inH; + /* Make main filters for video stream */ var mainFilters = new List(); @@ -4507,10 +4607,10 @@ namespace MediaBrowser.Controller.MediaEncoding } var outFormat = doOclTonemap ? "yuv420p10le" : "nv12"; - var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); // sw scale mainFilters.Add(swScaleFilter); - mainFilters.Add("format=" + outFormat); + mainFilters.Add($"format={outFormat}"); // keep video at memory except ocl tonemap, // since the overhead caused by hwupload >>> using sw filter. @@ -4530,8 +4630,14 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add(deintFilter); } + // hw transpose + if (doVaVppTranspose) + { + mainFilters.Add($"transpose_vaapi=dir={tranposeDir}"); + } + var outFormat = doTonemap ? string.Empty : "nv12"; - var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", outFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); // allocate extra pool sizes for vaapi vpp if (!string.IsNullOrEmpty(hwScaleFilter)) @@ -4553,7 +4659,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (doOclTonemap && isVaapiDecoder) { // map from vaapi to opencl via vaapi-opencl interop(Intel only). - mainFilters.Add("hwmap=derive_device=opencl"); + mainFilters.Add("hwmap=derive_device=opencl:mode=read"); } // ocl tonemap @@ -4567,7 +4673,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // OUTPUT vaapi(nv12) surface(vram) // reverse-mapping via vaapi-opencl interop. - mainFilters.Add("hwmap=derive_device=vaapi:reverse=1"); + mainFilters.Add("hwmap=derive_device=vaapi:mode=write:reverse=1"); mainFilters.Add("format=vaapi"); } @@ -4618,7 +4724,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (hasGraphicalSubs) { // overlay_vaapi can handle overlay scaling, setup a smaller height to reduce transfer overhead - var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, 1080); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, 1080); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -4627,7 +4733,7 @@ namespace MediaBrowser.Controller.MediaEncoding var framerate = state.VideoStream?.RealFrameRate; var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10; - var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, 1080, subFramerate); + var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, 1080, subFramerate); var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true); subFilters.Add(alphaSrcFilter); subFilters.Add("format=bgra"); @@ -4636,9 +4742,9 @@ namespace MediaBrowser.Controller.MediaEncoding subFilters.Add("hwupload=derive_device=vaapi"); - var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var (overlayW, overlayH) = GetFixedOutputSize(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); var overlaySize = (overlayW.HasValue && overlayH.HasValue) - ? (":w=" + overlayW.Value + ":h=" + overlayH.Value) + ? $":w={overlayW.Value}:h={overlayH.Value}" : string.Empty; var overlayVaapiFilter = string.Format( CultureInfo.InvariantCulture, @@ -4651,7 +4757,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); @@ -4696,6 +4802,13 @@ namespace MediaBrowser.Controller.MediaEncoding && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + var rotation = state.VideoStream?.Rotation ?? 0; + var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVkTranspose = isVaapiDecoder && !string.IsNullOrEmpty(tranposeDir); + var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isVaapiDecoder && doVkTranspose)); + var swpInW = swapWAndH ? inH : inW; + var swpInH = swapWAndH ? inW : inH; + /* Make main filters for video stream */ var mainFilters = new List(); @@ -4720,7 +4833,7 @@ namespace MediaBrowser.Controller.MediaEncoding else { // sw scale - var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); mainFilters.Add(swScaleFilter); mainFilters.Add("format=nv12"); } @@ -4728,7 +4841,7 @@ namespace MediaBrowser.Controller.MediaEncoding else if (isVaapiDecoder) { // INPUT vaapi surface(vram) - if (doVkTonemap || hasSubs) + if (doVkTranspose || doVkTonemap || hasSubs) { // map from vaapi to vulkan/drm via interop (Polaris/gfx8+). mainFilters.Add("hwmap=derive_device=vulkan"); @@ -4744,15 +4857,28 @@ namespace MediaBrowser.Controller.MediaEncoding } // hw scale - var hwScaleFilter = GetHwScaleFilter("vaapi", "nv12", inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", "nv12", false, inW, inH, reqW, reqH, reqMaxW, reqMaxH); mainFilters.Add(hwScaleFilter); } } + // vk transpose + if (doVkTranspose) + { + if (string.Equals(tranposeDir, "reversal", StringComparison.OrdinalIgnoreCase)) + { + mainFilters.Add("flip_vulkan"); + } + else + { + mainFilters.Add($"transpose_vulkan=dir={tranposeDir}"); + } + } + // vk libplacebo if (doVkTonemap || hasSubs) { - var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); mainFilters.Add(libplaceboFilter); } @@ -4796,7 +4922,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -4805,7 +4931,7 @@ namespace MediaBrowser.Controller.MediaEncoding var framerate = state.VideoStream?.RealFrameRate; var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10; - var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate); + var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH, subFramerate); var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true); subFilters.Add(alphaSrcFilter); subFilters.Add("format=bgra"); @@ -4877,6 +5003,11 @@ namespace MediaBrowser.Controller.MediaEncoding var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; + var rotation = state.VideoStream?.Rotation ?? 0; + var swapWAndH = Math.Abs(rotation) == 90 && isSwDecoder; + var swpInW = swapWAndH ? inH : inW; + var swpInH = swapWAndH ? inW : inH; + /* Make main filters for video stream */ var mainFilters = new List(); @@ -4894,7 +5025,7 @@ namespace MediaBrowser.Controller.MediaEncoding } outFormat = doOclTonemap ? "yuv420p10le" : "nv12"; - var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); // sw scale mainFilters.Add(swScaleFilter); mainFilters.Add("format=" + outFormat); @@ -4918,7 +5049,7 @@ namespace MediaBrowser.Controller.MediaEncoding } outFormat = doOclTonemap ? string.Empty : "nv12"; - var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", outFormat, false, inW, inH, reqW, reqH, reqMaxW, reqMaxH); // allocate extra pool sizes for vaapi vpp if (!string.IsNullOrEmpty(hwScaleFilter)) @@ -5014,7 +5145,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); @@ -5068,6 +5199,14 @@ namespace MediaBrowser.Controller.MediaEncoding string vidDecoder, string vidEncoder) { + var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase); + + if (!isVtEncoder) + { + // should not happen. + return (null, null, null); + } + var inW = state.VideoStream?.Width; var inH = state.VideoStream?.Height; var reqW = state.BaseRequest.Width; @@ -5076,14 +5215,19 @@ namespace MediaBrowser.Controller.MediaEncoding var reqMaxH = state.BaseRequest.MaxHeight; var threeDFormat = state.MediaSource.Video3DFormat; - var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase); - var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); var doDeintH2645 = doDeintH264 || doDeintHevc; var doVtTonemap = IsVideoToolboxTonemapAvailable(state, options); var doMetalTonemap = !doVtTonemap && IsHwTonemapAvailable(state, options); + var rotation = state.VideoStream?.Rotation ?? 0; + var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVtTranspose = !string.IsNullOrEmpty(tranposeDir) && _mediaEncoder.SupportsFilter("transpose_vt"); + var swapWAndH = Math.Abs(rotation) == 90 && doVtTranspose; + var swpInW = swapWAndH ? inH : inW; + var swpInH = swapWAndH ? inW : inH; + var scaleFormat = string.Empty; // Use P010 for Metal tone mapping, otherwise force an 8bit output. if (!string.Equals(state.VideoStream.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase)) @@ -5101,7 +5245,7 @@ namespace MediaBrowser.Controller.MediaEncoding } } - var hwScaleFilter = GetHwScaleFilter("vt", scaleFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var hwScaleFilter = GetHwScaleFilter("scale", "vt", scaleFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; @@ -5110,12 +5254,6 @@ namespace MediaBrowser.Controller.MediaEncoding && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); - if (!isVtEncoder) - { - // should not happen. - return (null, null, null); - } - /* Make main filters for video stream */ var mainFilters = new List(); @@ -5131,6 +5269,12 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add(deintFilter); } + // hw transpose + if (doVtTranspose) + { + mainFilters.Add($"transpose_vt=dir={tranposeDir}"); + } + if (doVtTonemap) { const string VtTonemapArgs = "color_matrix=bt709:color_primaries=bt709:color_transfer=bt709"; @@ -5159,7 +5303,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -5168,7 +5312,7 @@ namespace MediaBrowser.Controller.MediaEncoding var framerate = state.VideoStream?.RealFrameRate; var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10; - var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate); + var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH, subFramerate); var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true); subFilters.Add(alphaSrcFilter); subFilters.Add("format=bgra"); @@ -5253,6 +5397,13 @@ namespace MediaBrowser.Controller.MediaEncoding && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + var rotation = state.VideoStream?.Rotation ?? 0; + var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doRkVppTranspose = !string.IsNullOrEmpty(tranposeDir); + var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isRkmppDecoder && doRkVppTranspose)); + var swpInW = swapWAndH ? inH : inW; + var swpInH = swapWAndH ? inW : inH; + /* Make main filters for video stream */ var mainFilters = new List(); @@ -5269,7 +5420,7 @@ namespace MediaBrowser.Controller.MediaEncoding } var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12"); - var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); if (!string.IsNullOrEmpty(swScaleFilter)) { swScaleFilter += ":flags=fast_bilinear"; @@ -5277,7 +5428,7 @@ namespace MediaBrowser.Controller.MediaEncoding // sw scale mainFilters.Add(swScaleFilter); - mainFilters.Add("format=" + outFormat); + mainFilters.Add($"format={outFormat}"); // keep video at memory except ocl tonemap, // since the overhead caused by hwupload >>> using sw filter. @@ -5292,21 +5443,29 @@ namespace MediaBrowser.Controller.MediaEncoding // INPUT rkmpp/drm surface(gem/dma-heap) var isFullAfbcPipeline = isDrmInDrmOut && !doOclTonemap; + var swapOutputWandH = doRkVppTranspose && swapWAndH; var outFormat = doOclTonemap ? "p010" : "nv12"; - var hwScaleFilter = GetHwScaleFilter("rkrga", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH); - var hwScaleFilter2 = GetHwScaleFilter("rkrga", string.Empty, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var hwScalePrefix = doRkVppTranspose ? "vpp" : "scale"; + var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var hwScaleFilter2 = GetHwScaleFilter(hwScalePrefix, "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); if (!hasSubs + || doRkVppTranspose || !isFullAfbcPipeline || !string.IsNullOrEmpty(hwScaleFilter2)) { + if (!string.IsNullOrEmpty(hwScaleFilter) && doRkVppTranspose) + { + hwScaleFilter += $":transpose={tranposeDir}"; + } + // try enabling AFBC to save DDR bandwidth if (!string.IsNullOrEmpty(hwScaleFilter) && isFullAfbcPipeline) { hwScaleFilter += ":afbc=1"; } - // hw scale + // hw transpose & scale mainFilters.Add(hwScaleFilter); } } @@ -5377,7 +5536,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -5387,7 +5546,7 @@ namespace MediaBrowser.Controller.MediaEncoding var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10; // alphasrc=s=1280x720:r=10:start=0,format=bgra,subtitles,hwupload - var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate); + var alphaSrcFilter = GetAlphaSrcFilter(state, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH, subFramerate); var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true); subFilters.Add(alphaSrcFilter); subFilters.Add("format=bgra"); @@ -5404,7 +5563,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } @@ -5806,6 +5965,11 @@ namespace MediaBrowser.Controller.MediaEncoding // Disable the extra internal copy in nvdec. We already handle it in filter chain. var nvdecNoInternalCopy = ffmpegVersion >= _minFFmpegHwaUnsafeOutput; + // Strip the display rotation side data from the transposed fmp4 output stream. + var stripRotationData = (state.VideoStream?.Rotation ?? 0) != 0 + && ffmpegVersion >= _minFFmpegDisplayRotationOption; + var stripRotationDataArgs = stripRotationData ? " -display_rotation 0" : string.Empty; + if (bitDepth == 10 && isCodecAvailable) { if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase) @@ -5830,13 +5994,13 @@ namespace MediaBrowser.Controller.MediaEncoding { if (isVaapiSupported && isCodecAvailable) { - return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi -noautorotate" + stripRotationDataArgs : string.Empty) + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty); } if (isD3d11Supported && isCodecAvailable) { - return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11 -noautorotate" + stripRotationDataArgs : string.Empty) + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 2" + (isAv1 ? " -c:v av1" : string.Empty); } } @@ -5844,7 +6008,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (isQsvSupported && isCodecAvailable) { - return " -hwaccel qsv" + (outputHwSurface ? " -hwaccel_output_format qsv" : string.Empty); + return " -hwaccel qsv" + (outputHwSurface ? " -hwaccel_output_format qsv -noautorotate" + stripRotationDataArgs : string.Empty); } } } @@ -5857,12 +6021,12 @@ namespace MediaBrowser.Controller.MediaEncoding if (options.EnableEnhancedNvdecDecoder) { // set -threads 1 to nvdec decoder explicitly since it doesn't implement threading support. - return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty) + return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda -noautorotate" + stripRotationDataArgs : string.Empty) + (nvdecNoInternalCopy ? " -hwaccel_flags +unsafe_output" : string.Empty) + " -threads 1" + (isAv1 ? " -c:v av1" : string.Empty); } // cuvid decoder doesn't have threading issue. - return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty); + return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda -noautorotate" + stripRotationDataArgs : string.Empty); } } @@ -5871,7 +6035,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (isD3d11Supported && isCodecAvailable) { - return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11 -noautorotate" + stripRotationDataArgs : string.Empty) + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty); } } @@ -5881,7 +6045,7 @@ namespace MediaBrowser.Controller.MediaEncoding && isVaapiSupported && isCodecAvailable) { - return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi -noautorotate" + stripRotationDataArgs : string.Empty) + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty); } @@ -5890,7 +6054,7 @@ namespace MediaBrowser.Controller.MediaEncoding && isVideotoolboxSupported && isCodecAvailable) { - return " -hwaccel videotoolbox" + (outputHwSurface ? " -hwaccel_output_format videotoolbox_vld" : string.Empty); + return " -hwaccel videotoolbox" + (outputHwSurface ? " -hwaccel_output_format videotoolbox_vld" : string.Empty) + "-noautorotate" + stripRotationDataArgs; } // Rockchip rkmpp @@ -5898,7 +6062,7 @@ namespace MediaBrowser.Controller.MediaEncoding && isRkmppSupported && isCodecAvailable) { - return " -hwaccel rkmpp" + (outputHwSurface ? " -hwaccel_output_format drm_prime" : string.Empty); + return " -hwaccel rkmpp" + (outputHwSurface ? " -hwaccel_output_format drm_prime -noautorotate" + stripRotationDataArgs : string.Empty); } return null; diff --git a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs index b1d319d21..a2b6e1d73 100644 --- a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs +++ b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs @@ -33,6 +33,11 @@ namespace MediaBrowser.Controller.MediaEncoding /// /// The overlay_vulkan_framesync. /// - OverlayVulkanFrameSync = 5 + OverlayVulkanFrameSync = 5, + + /// + /// The transpose_opencl_reversal. + /// + TransposeOpenclReversal = 6 } } diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index ae0284e3a..7226e0bf1 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -110,25 +110,31 @@ namespace MediaBrowser.MediaEncoding.Encoder "yadif_cuda", "tonemap_cuda", "overlay_cuda", + "transpose_cuda", "hwupload_cuda", // opencl "scale_opencl", "tonemap_opencl", "overlay_opencl", + "transpose_opencl", // vaapi "scale_vaapi", "deinterlace_vaapi", "tonemap_vaapi", "procamp_vaapi", "overlay_vaapi", + "transpose_vaapi", "hwupload_vaapi", // vulkan "libplacebo", "scale_vulkan", "overlay_vulkan", + "transpose_vulkan", + "flip_vulkan", // videotoolbox "yadif_videotoolbox", "scale_vt", + "transpose_vt", "overlay_videotoolbox", "tonemap_videotoolbox", // rkrga @@ -144,7 +150,8 @@ namespace MediaBrowser.MediaEncoding.Encoder { 2, new string[] { "tonemap_opencl", "bt2390" } }, { 3, new string[] { "overlay_opencl", "Action to take when encountering EOF from secondary input" } }, { 4, new string[] { "overlay_vaapi", "Action to take when encountering EOF from secondary input" } }, - { 5, new string[] { "overlay_vulkan", "Action to take when encountering EOF from secondary input" } } + { 5, new string[] { "overlay_vulkan", "Action to take when encountering EOF from secondary input" } }, + { 6, new string[] { "transpose_opencl", "rotate by half-turn" } } }; // These are the library versions that corresponds to our minimum ffmpeg version 4.4 according to the version table below From d4472074896a8ed3f66a6eb2c7cd438b97787ae4 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Wed, 28 Aug 2024 03:20:04 +0800 Subject: [PATCH 6/6] Apply suggestions from code review Signed-off-by: nyanmisaka --- MediaBrowser.MediaEncoding/Probing/MediaStreamInfoSideData.cs | 2 +- MediaBrowser.Model/Entities/MediaStream.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfoSideData.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfoSideData.cs index 65d1ddf89..0b5dd1d1b 100644 --- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfoSideData.cs +++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfoSideData.cs @@ -71,7 +71,7 @@ namespace MediaBrowser.MediaEncoding.Probing public int? DvBlSignalCompatibilityId { get; set; } /// - /// Gets or sets the Rotation. + /// Gets or sets the Rotation in degrees. /// /// The Rotation. [JsonPropertyName("rotation")] diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 7e227c5aa..a0e8c39be 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -124,7 +124,7 @@ namespace MediaBrowser.Model.Entities public int? DvBlSignalCompatibilityId { get; set; } /// - /// Gets or sets the Rotation. + /// Gets or sets the Rotation in degrees. /// /// The video rotation. public int? Rotation { get; set; }