From 5a8a19e07b0de471d4c0762d1692a29b06d5bfd6 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 8 Sep 2024 01:16:23 +0800 Subject: [PATCH 001/159] Add MediaStream.ReferenceFrameRate for problematic video files (#12603) Co-authored-by: Nyanmisaka --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 2 +- .../MediaEncoding/EncodingHelper.cs | 10 +++++----- .../MediaEncoding/EncodingJobInfo.cs | 2 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 8 ++++---- MediaBrowser.Model/Dlna/StreamInfo.cs | 2 +- MediaBrowser.Model/Entities/MediaStream.cs | 17 +++++++++++++++++ .../Savers/BaseNfoSaver.cs | 2 +- 7 files changed, 30 insertions(+), 13 deletions(-) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index ba92d811cf..a58da17d59 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -738,7 +738,7 @@ public class DynamicHlsHelper { var width = state.VideoStream.Width ?? 0; var height = state.VideoStream.Height ?? 0; - var framerate = state.VideoStream.AverageFrameRate ?? 30; + var framerate = state.VideoStream.ReferenceFrameRate ?? 30; var bitDepth = state.VideoStream.BitDepth ?? 8; return HlsCodecStringHelpers.GetVp9String( width, diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 24cd141dcd..220b5d57b9 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1534,7 +1534,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (maxrate.HasValue && state.VideoStream is not null) { - var contentRate = state.VideoStream.AverageFrameRate ?? state.VideoStream.RealFrameRate; + var contentRate = state.VideoStream.ReferenceFrameRate; if (contentRate.HasValue && contentRate.Value > maxrate.Value) { @@ -2218,7 +2218,7 @@ namespace MediaBrowser.Controller.MediaEncoding var requestedFramerate = request.MaxFramerate ?? request.Framerate; if (requestedFramerate.HasValue) { - var videoFrameRate = videoStream.AverageFrameRate ?? videoStream.RealFrameRate; + var videoFrameRate = videoStream.ReferenceFrameRate; if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value) { @@ -3234,7 +3234,7 @@ namespace MediaBrowser.Controller.MediaEncoding public static string GetSwDeinterlaceFilter(EncodingJobInfo state, EncodingOptions options) { - var doubleRateDeint = options.DeinterlaceDoubleRate && state.VideoStream?.AverageFrameRate <= 30; + var doubleRateDeint = options.DeinterlaceDoubleRate && state.VideoStream?.ReferenceFrameRate <= 30; return string.Format( CultureInfo.InvariantCulture, "{0}={1}:-1:0", @@ -3244,7 +3244,7 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetHwDeinterlaceFilter(EncodingJobInfo state, EncodingOptions options, string hwDeintSuffix) { - var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.AverageFrameRate ?? 60) <= 30; + var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.ReferenceFrameRate ?? 60) <= 30; if (hwDeintSuffix.Contains("cuda", StringComparison.OrdinalIgnoreCase)) { var useBwdif = string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase) @@ -3598,7 +3598,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isSwEncoder = !isNvencEncoder; var isCuInCuOut = isNvDecoder && isNvencEncoder; - var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.AverageFrameRate ?? 60) <= 30; + var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.ReferenceFrameRate ?? 60) <= 30; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); var doDeintH2645 = doDeintH264 || doDeintHevc; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 72df7151da..caa312987d 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -305,7 +305,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec)) { - return VideoStream is null ? null : (VideoStream.AverageFrameRate ?? VideoStream.RealFrameRate); + return VideoStream?.ReferenceFrameRate; } return BaseRequest.MaxFramerate ?? BaseRequest.Framerate; diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 7f387bfaae..cd18fea123 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -810,7 +810,7 @@ namespace MediaBrowser.Model.Dlna if (options.AllowVideoStreamCopy) { // prefer direct copy profile - float videoFramerate = videoStream?.AverageFrameRate ?? videoStream?.RealFrameRate ?? 0; + float videoFramerate = videoStream?.ReferenceFrameRate ?? 0; TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp; int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); @@ -875,7 +875,7 @@ namespace MediaBrowser.Model.Dlna playlistItem.VideoCodecs = videoCodecs; // Copy video codec options as a starting point, this applies to transcode and direct-stream - playlistItem.MaxFramerate = videoStream?.AverageFrameRate; + playlistItem.MaxFramerate = videoStream?.ReferenceFrameRate; var qualifier = videoStream?.Codec; if (videoStream?.Level is not null) { @@ -949,7 +949,7 @@ namespace MediaBrowser.Model.Dlna double? videoLevel = videoStream?.Level; string? videoProfile = videoStream?.Profile; VideoRangeType? videoRangeType = videoStream?.VideoRangeType; - float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; + float videoFramerate = videoStream is null ? 0 : videoStream.ReferenceFrameRate ?? 0; bool? isAnamorphic = videoStream?.IsAnamorphic; bool? isInterlaced = videoStream?.IsInterlaced; string? videoCodecTag = videoStream?.CodecTag; @@ -1208,7 +1208,7 @@ namespace MediaBrowser.Model.Dlna double? videoLevel = videoStream?.Level; string? videoProfile = videoStream?.Profile; VideoRangeType? videoRangeType = videoStream?.VideoRangeType; - float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; + float videoFramerate = videoStream is null ? 0 : videoStream.ReferenceFrameRate ?? 0; bool? isAnamorphic = videoStream?.IsAnamorphic; bool? isInterlaced = videoStream?.IsInterlaced; string? videoCodecTag = videoStream?.CodecTag; diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index c8a341d413..8232ee3fe5 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -217,7 +217,7 @@ namespace MediaBrowser.Model.Dlna var stream = TargetVideoStream; return MaxFramerate.HasValue && !IsDirectStream ? MaxFramerate - : stream is null ? null : stream.AverageFrameRate ?? stream.RealFrameRate; + : stream?.ReferenceFrameRate; } } diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index a0e8c39bee..b5d19edd66 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -525,6 +525,23 @@ namespace MediaBrowser.Model.Entities /// The real frame rate. public float? RealFrameRate { get; set; } + /// + /// Gets the framerate used as reference. + /// Prefer AverageFrameRate, if that is null or an unrealistic value + /// then fallback to RealFrameRate. + /// + /// The reference frame rate. + public float? ReferenceFrameRate + { + get + { + // In some cases AverageFrameRate for videos will be read as 1000fps even if it is not. + // This is probably due to a library compatability issue. + // See https://github.com/jellyfin/jellyfin/pull/12603#discussion_r1748044018 for more info. + return AverageFrameRate < 1000 ? AverageFrameRate : RealFrameRate; + } + } + /// /// Gets or sets the profile. /// diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index a547779de4..2afec3f6cd 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -348,7 +348,7 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("aspectratio", stream.AspectRatio); } - var framerate = stream.AverageFrameRate ?? stream.RealFrameRate; + var framerate = stream.ReferenceFrameRate; if (framerate.HasValue) { From ced2d21f7b40c33a2b2874817f47dc250417f020 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 8 Sep 2024 01:17:16 +0800 Subject: [PATCH 002/159] Add SUPPLEMENTAL-CODECS for Dolby Vision video with fallbacks (#12605) --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 44 ++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index a58da17d59..0e620e72a9 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -304,6 +304,8 @@ public class DynamicHlsHelper AppendPlaylistCodecsField(playlistBuilder, state); + AppendPlaylistSupplementalCodecsField(playlistBuilder, state); + AppendPlaylistResolutionField(playlistBuilder, state); AppendPlaylistFramerateField(playlistBuilder, state); @@ -406,6 +408,48 @@ public class DynamicHlsHelper } } + /// + /// Appends a SUPPLEMENTAL-CODECS field containing formatted strings of + /// the active streams output Dolby Vision Videos. + /// + /// + /// + /// StringBuilder to append the field to. + /// StreamState of the current stream. + private void AppendPlaylistSupplementalCodecsField(StringBuilder builder, StreamState state) + { + // Dolby Vision currently cannot exist when transcoding + if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) + { + return; + } + + var dvProfile = state.VideoStream.DvProfile; + var dvLevel = state.VideoStream.DvLevel; + var dvRangeString = state.VideoStream.VideoRangeType switch + { + VideoRangeType.DOVIWithHDR10 => "db1p", + VideoRangeType.DOVIWithHLG => "db4h", + _ => string.Empty + }; + + if (dvProfile is null || dvLevel is null || string.IsNullOrEmpty(dvRangeString)) + { + return; + } + + var dvFourCc = string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase) ? "dav1" : "dvh1"; + builder.Append(",SUPPLEMENTAL-CODECS=\"") + .Append(dvFourCc) + .Append('.') + .Append(dvProfile.Value.ToString("D2", CultureInfo.InvariantCulture)) + .Append('.') + .Append(dvLevel.Value.ToString("D2", CultureInfo.InvariantCulture)) + .Append('/') + .Append(dvRangeString) + .Append('"'); + } + /// /// Appends a RESOLUTION field containing the resolution of the output stream. /// From d0567fc8c61e73277578c4d5c4a8fb683b945514 Mon Sep 17 00:00:00 2001 From: llutic <113514011+llutic@users.noreply.github.com> Date: Sat, 7 Sep 2024 19:18:18 +0200 Subject: [PATCH 003/159] Add support DoVi Profile 10 (#11559) --- MediaBrowser.Model/Entities/MediaStream.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index b5d19edd66..85c1f797b4 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -200,7 +200,8 @@ namespace MediaBrowser.Model.Entities || dvProfile == 5 || dvProfile == 7 || dvProfile == 8 - || dvProfile == 9)) + || dvProfile == 9 + || dvProfile == 10)) { var title = "Dolby Vision Profile " + dvProfile; @@ -777,7 +778,7 @@ namespace MediaBrowser.Model.Entities var blPresentFlag = BlPresentFlag == 1; var dvBlCompatId = DvBlSignalCompatibilityId; - var isDoViProfile = dvProfile == 5 || dvProfile == 7 || dvProfile == 8; + var isDoViProfile = dvProfile == 5 || dvProfile == 7 || dvProfile == 8 || dvProfile == 10; var isDoViFlag = rpuPresentFlag && blPresentFlag && (dvBlCompatId == 0 || dvBlCompatId == 1 || dvBlCompatId == 4 || dvBlCompatId == 2 || dvBlCompatId == 6); if ((isDoViProfile && isDoViFlag) @@ -800,6 +801,17 @@ namespace MediaBrowser.Model.Entities _ => (VideoRange.SDR, VideoRangeType.SDR) }, 7 => (VideoRange.HDR, VideoRangeType.HDR10), + 10 => dvBlCompatId switch + { + 0 => (VideoRange.HDR, VideoRangeType.DOVI), + 1 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10), + 2 => (VideoRange.SDR, VideoRangeType.DOVIWithSDR), + 4 => (VideoRange.HDR, VideoRangeType.DOVIWithHLG), + // While not in Dolby Spec, Profile 8 CCid 6 media are possible to create, and since CCid 6 stems from Bluray (Profile 7 originally) an HDR10 base layer is guaranteed to exist. + 6 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10), + // There is no other case to handle here as per Dolby Spec. Default case included for completeness and linting purposes + _ => (VideoRange.SDR, VideoRangeType.SDR) + }, _ => (VideoRange.SDR, VideoRangeType.SDR) }; } From 5d4f71eb9a2095a9fc42f9098bad23a870a50307 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Sun, 8 Sep 2024 01:19:26 +0800 Subject: [PATCH 004/159] Enable tone-mapping and HLS remuxing for DoVi Profile 10 in AV1 (#12604) --- .../Controllers/DynamicHlsController.cs | 22 +++++++++++++------ .../MediaEncoding/EncodingHelper.cs | 3 +-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 662e2acbc7..b9ef189e98 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1797,10 +1797,11 @@ public class DynamicHlsController : BaseJellyfinApiController var args = "-codec:v:0 " + codec; - if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) + var isActualOutputVideoCodecAv1 = string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase); + var isActualOutputVideoCodecHevc = string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase); + + if (isActualOutputVideoCodecHevc || isActualOutputVideoCodecAv1) { var requestedRange = state.GetRequestedRangeTypes(state.ActualOutputVideoCodec); var requestHasDOVI = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase); @@ -1814,10 +1815,17 @@ public class DynamicHlsController : BaseJellyfinApiController || (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG && requestHasDOVIWithHLG) || (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithSDR && requestHasDOVIWithSDR))) { - // Prefer dvh1 to dvhe - args += " -tag:v:0 dvh1 -strict -2"; + if (isActualOutputVideoCodecHevc) + { + // Prefer dvh1 to dvhe + args += " -tag:v:0 dvh1 -strict -2"; + } + else if (isActualOutputVideoCodecAv1) + { + args += " -tag:v:0 dav1 -strict -2"; + } } - else + else if (isActualOutputVideoCodecHevc) { // Prefer hvc1 to hev1 args += " -tag:v:0 hvc1"; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 220b5d57b9..52faf22f2d 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -315,8 +315,7 @@ namespace MediaBrowser.Controller.MediaEncoding return false; } - if (string.Equals(state.VideoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase) - && state.VideoStream.VideoRange == VideoRange.HDR + if (state.VideoStream.VideoRange == VideoRange.HDR && state.VideoStream.VideoRangeType == VideoRangeType.DOVI) { // Only native SW decoder and HW accelerator can parse dovi rpu. From df00909b85db0c312879df404049dd98508781d2 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 8 Sep 2024 01:21:14 +0800 Subject: [PATCH 005/159] Backport #12562 and #12521 (#12602) Co-authored-by: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com> Co-authored-by: Nyanmisaka --- .../MediaEncoding/EncodingHelper.cs | 21 ++++++++++++++++++- MediaBrowser.Model/Dlna/StreamBuilder.cs | 12 +++++------ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 52faf22f2d..d6ad7e2b35 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1914,7 +1914,26 @@ namespace MediaBrowser.Controller.MediaEncoding } var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault() ?? string.Empty; - profile = WhiteSpaceRegex().Replace(profile, string.Empty); + profile = WhiteSpaceRegex().Replace(profile, string.Empty).ToLowerInvariant(); + + var videoProfiles = Array.Empty(); + if (string.Equals("h264", targetVideoCodec, StringComparison.OrdinalIgnoreCase)) + { + videoProfiles = _videoProfilesH264; + } + else if (string.Equals("hevc", targetVideoCodec, StringComparison.OrdinalIgnoreCase)) + { + videoProfiles = _videoProfilesH265; + } + else if (string.Equals("av1", targetVideoCodec, StringComparison.OrdinalIgnoreCase)) + { + videoProfiles = _videoProfilesAv1; + } + + if (!videoProfiles.Contains(profile, StringComparison.OrdinalIgnoreCase)) + { + profile = string.Empty; + } // We only transcode to HEVC 8-bit for now, force Main Profile. if (profile.Contains("main10", StringComparison.OrdinalIgnoreCase) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index cd18fea123..ad00149e0a 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -925,7 +925,7 @@ namespace MediaBrowser.Model.Dlna { audioStream = directAudioStream; playlistItem.AudioStreamIndex = audioStream.Index; - playlistItem.AudioCodecs = new[] { audioStream.Codec }; + playlistItem.AudioCodecs = audioCodecs = new[] { audioStream.Codec }; // Copy matching audio codec options playlistItem.AudioSampleRate = audioStream.SampleRate; @@ -966,15 +966,14 @@ namespace MediaBrowser.Model.Dlna var appliedVideoConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.Video && - i.ContainsAnyCodec(videoStream?.Codec, container, useSubContainer) && + i.ContainsAnyCodec(videoCodecs, container, useSubContainer) && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))) // Reverse codec profiles for backward compatibility - first codec profile has higher priority .Reverse(); foreach (var i in appliedVideoConditions) { - var transcodingVideoCodecs = ContainerProfile.SplitValue(videoCodec); - foreach (var transcodingVideoCodec in transcodingVideoCodecs) + foreach (var transcodingVideoCodec in videoCodecs) { if (i.ContainsAnyCodec(transcodingVideoCodec, container, useSubContainer)) { @@ -999,15 +998,14 @@ namespace MediaBrowser.Model.Dlna var appliedAudioConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.VideoAudio && - i.ContainsAnyCodec(audioStream?.Codec, container) && + i.ContainsAnyCodec(audioCodecs, container) && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio))) // Reverse codec profiles for backward compatibility - first codec profile has higher priority .Reverse(); foreach (var codecProfile in appliedAudioConditions) { - var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec); - foreach (var transcodingAudioCodec in transcodingAudioCodecs) + foreach (var transcodingAudioCodec in audioCodecs) { if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container)) { From 675a8a9ec91da47e37ace6161ba5a5a0e20a7839 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sat, 7 Sep 2024 19:22:31 +0200 Subject: [PATCH 006/159] Remove left-over network path references (#12446) --- .../Library/LibraryManager.cs | 35 +------------------ .../Configuration/MediaPathInfo.cs | 2 -- .../Configuration/ServerConfiguration.cs | 2 -- 3 files changed, 1 insertion(+), 38 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 48d24385e9..28f7ed6598 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2725,33 +2725,9 @@ namespace Emby.Server.Implementations.Library public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem) { - string? newPath; - if (ownerItem is not null) - { - var libraryOptions = GetLibraryOptions(ownerItem); - if (libraryOptions is not null) - { - foreach (var pathInfo in libraryOptions.PathInfos) - { - if (path.TryReplaceSubPath(pathInfo.Path, pathInfo.NetworkPath, out newPath)) - { - return newPath; - } - } - } - } - - var metadataPath = _configurationManager.Configuration.MetadataPath; - var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath; - - if (path.TryReplaceSubPath(metadataPath, metadataNetworkPath, out newPath)) - { - return newPath; - } - foreach (var map in _configurationManager.Configuration.PathSubstitutions) { - if (path.TryReplaceSubPath(map.From, map.To, out newPath)) + if (path.TryReplaceSubPath(map.From, map.To, out var newPath)) { return newPath; } @@ -3070,15 +3046,6 @@ namespace Emby.Server.Implementations.Library SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions); - foreach (var originalPathInfo in libraryOptions.PathInfos) - { - if (string.Equals(mediaPath.Path, originalPathInfo.Path, StringComparison.Ordinal)) - { - originalPathInfo.NetworkPath = mediaPath.NetworkPath; - break; - } - } - CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions); } diff --git a/MediaBrowser.Model/Configuration/MediaPathInfo.cs b/MediaBrowser.Model/Configuration/MediaPathInfo.cs index a7bc435901..25a5d5606b 100644 --- a/MediaBrowser.Model/Configuration/MediaPathInfo.cs +++ b/MediaBrowser.Model/Configuration/MediaPathInfo.cs @@ -16,7 +16,5 @@ namespace MediaBrowser.Model.Configuration } public string Path { get; set; } - - public string? NetworkPath { get; set; } } } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 52f7e53b81..5ad588200b 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -96,8 +96,6 @@ public class ServerConfiguration : BaseApplicationConfiguration /// The metadata path. public string MetadataPath { get; set; } = string.Empty; - public string MetadataNetworkPath { get; set; } = string.Empty; - /// /// Gets or sets the preferred metadata language. /// From c56dbc1c4410e1b0ec31ca901809b6f627bbb6ed Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sat, 7 Sep 2024 19:23:48 +0200 Subject: [PATCH 007/159] Enhance Trickplay (#11883) --- .../IO/ManagedFileSystem.cs | 38 ++-- .../Localization/Core/en-US.json | 4 +- .../Controllers/TrickplayController.cs | 7 +- .../Trickplay/TrickplayManager.cs | 181 +++++++++++++++--- Jellyfin.Server/Migrations/MigrationRunner.cs | 1 + .../Migrations/Routines/MoveTrickplayFiles.cs | 73 +++++++ .../Providers/MetadataRefreshOptions.cs | 7 + .../Trickplay/ITrickplayManager.cs | 34 +++- .../Configuration/LibraryOptions.cs | 4 + MediaBrowser.Model/IO/IFileSystem.cs | 7 + .../Trickplay/TrickplayImagesTask.cs | 3 +- .../Trickplay/TrickplayMoveImagesTask.cs | 110 +++++++++++ .../Trickplay/TrickplayProvider.cs | 6 +- 13 files changed, 422 insertions(+), 53 deletions(-) create mode 100644 Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs create mode 100644 MediaBrowser.Providers/Trickplay/TrickplayMoveImagesTask.cs diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 28bb29df85..4b68f21d55 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -149,6 +149,26 @@ namespace Emby.Server.Implementations.IO } } + /// + public void MoveDirectory(string source, string destination) + { + try + { + Directory.Move(source, destination); + } + catch (IOException) + { + // Cross device move requires a copy + Directory.CreateDirectory(destination); + foreach (string file in Directory.GetFiles(source)) + { + File.Copy(file, Path.Combine(destination, Path.GetFileName(file)), true); + } + + Directory.Delete(source, true); + } + } + /// /// Returns a object for the specified file or directory path. /// @@ -327,11 +347,7 @@ namespace Emby.Server.Implementations.IO } } - /// - /// Gets the creation time UTC. - /// - /// The path. - /// DateTime. + /// public virtual DateTime GetCreationTimeUtc(string path) { return GetCreationTimeUtc(GetFileSystemInfo(path)); @@ -368,11 +384,7 @@ namespace Emby.Server.Implementations.IO } } - /// - /// Gets the last write time UTC. - /// - /// The path. - /// DateTime. + /// public virtual DateTime GetLastWriteTimeUtc(string path) { return GetLastWriteTimeUtc(GetFileSystemInfo(path)); @@ -446,11 +458,7 @@ namespace Emby.Server.Implementations.IO File.SetAttributes(path, attributes); } - /// - /// Swaps the files. - /// - /// The file1. - /// The file2. + /// public virtual void SwapFiles(string file1, string file2) { ArgumentException.ThrowIfNullOrEmpty(file1); diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index d1410ef5e6..d248fc303b 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -131,5 +131,7 @@ "TaskKeyframeExtractor": "Keyframe Extractor", "TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.", "TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists", - "TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist." + "TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist.", + "TaskMoveTrickplayImages": "Migrate Trickplay Image Location", + "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings." } diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs index 60d49af9e3..c1ff0f3401 100644 --- a/Jellyfin.Api/Controllers/TrickplayController.cs +++ b/Jellyfin.Api/Controllers/TrickplayController.cs @@ -80,7 +80,7 @@ public class TrickplayController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] - public ActionResult GetTrickplayTileImage( + public async Task GetTrickplayTileImageAsync( [FromRoute, Required] Guid itemId, [FromRoute, Required] int width, [FromRoute, Required] int index, @@ -92,8 +92,9 @@ public class TrickplayController : BaseJellyfinApiController return NotFound(); } - var path = _trickplayManager.GetTrickplayTilePath(item, width, index); - if (System.IO.File.Exists(path)) + var saveWithMedia = _libraryManager.GetLibraryOptions(item).SaveTrickplayWithMedia; + var path = await _trickplayManager.GetTrickplayTilePathAsync(item, width, index, saveWithMedia).ConfigureAwait(false); + if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) { Response.Headers.ContentDisposition = "attachment"; return PhysicalFile(path, MediaTypeNames.Image.Jpeg); diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index bb32b7c20e..861037c1fe 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -76,7 +76,65 @@ public class TrickplayManager : ITrickplayManager } /// - public async Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken) + public async Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions? libraryOptions, CancellationToken cancellationToken) + { + var options = _config.Configuration.TrickplayOptions; + if (!CanGenerateTrickplay(video, options.Interval)) + { + return; + } + + var existingTrickplayResolutions = await GetTrickplayResolutions(video.Id).ConfigureAwait(false); + foreach (var resolution in existingTrickplayResolutions) + { + cancellationToken.ThrowIfCancellationRequested(); + var existingResolution = resolution.Key; + var tileWidth = resolution.Value.TileWidth; + var tileHeight = resolution.Value.TileHeight; + var shouldBeSavedWithMedia = libraryOptions is null ? false : libraryOptions.SaveTrickplayWithMedia; + var localOutputDir = GetTrickplayDirectory(video, tileWidth, tileHeight, existingResolution, false); + var mediaOutputDir = GetTrickplayDirectory(video, tileWidth, tileHeight, existingResolution, true); + if (shouldBeSavedWithMedia && Directory.Exists(localOutputDir)) + { + var localDirFiles = Directory.GetFiles(localOutputDir); + var mediaDirExists = Directory.Exists(mediaOutputDir); + if (localDirFiles.Length > 0 && ((mediaDirExists && Directory.GetFiles(mediaOutputDir).Length == 0) || !mediaDirExists)) + { + // Move images from local dir to media dir + MoveContent(localOutputDir, mediaOutputDir); + _logger.LogInformation("Moved trickplay images for {ItemName} to {Location}", video.Name, mediaOutputDir); + } + } + else if (Directory.Exists(mediaOutputDir)) + { + var mediaDirFiles = Directory.GetFiles(mediaOutputDir); + var localDirExists = Directory.Exists(localOutputDir); + if (mediaDirFiles.Length > 0 && ((localDirExists && Directory.GetFiles(localOutputDir).Length == 0) || !localDirExists)) + { + // Move images from media dir to local dir + MoveContent(mediaOutputDir, localOutputDir); + _logger.LogInformation("Moved trickplay images for {ItemName} to {Location}", video.Name, localOutputDir); + } + } + } + } + + private void MoveContent(string sourceFolder, string destinationFolder) + { + _fileSystem.MoveDirectory(sourceFolder, destinationFolder); + var parent = Directory.GetParent(sourceFolder); + if (parent is not null) + { + var parentContent = Directory.GetDirectories(parent.FullName); + if (parentContent.Length == 0) + { + Directory.Delete(parent.FullName); + } + } + } + + /// + public async Task RefreshTrickplayDataAsync(Video video, bool replace, LibraryOptions? libraryOptions, CancellationToken cancellationToken) { _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace); @@ -95,6 +153,7 @@ public class TrickplayManager : ITrickplayManager replace, width, options, + libraryOptions, cancellationToken).ConfigureAwait(false); } } @@ -104,6 +163,7 @@ public class TrickplayManager : ITrickplayManager bool replace, int width, TrickplayOptions options, + LibraryOptions? libraryOptions, CancellationToken cancellationToken) { if (!CanGenerateTrickplay(video, options.Interval)) @@ -144,14 +204,53 @@ public class TrickplayManager : ITrickplayManager actualWidth = 2 * ((int)mediaSource.VideoStream.Width / 2); } - var outputDir = GetTrickplayDirectory(video, actualWidth); + var tileWidth = options.TileWidth; + var tileHeight = options.TileHeight; + var saveWithMedia = libraryOptions is null ? false : libraryOptions.SaveTrickplayWithMedia; + var outputDir = GetTrickplayDirectory(video, tileWidth, tileHeight, actualWidth, saveWithMedia); - if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(actualWidth)) + // Import existing trickplay tiles + if (!replace && Directory.Exists(outputDir)) { - _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting", video.Id); - return; + var existingFiles = Directory.GetFiles(outputDir); + if (existingFiles.Length > 0) + { + var hasTrickplayResolution = await HasTrickplayResolutionAsync(video.Id, actualWidth).ConfigureAwait(false); + if (hasTrickplayResolution) + { + _logger.LogDebug("Found existing trickplay files for {ItemId}.", video.Id); + return; + } + + // Import tiles + var localTrickplayInfo = new TrickplayInfo + { + ItemId = video.Id, + Width = width, + Interval = options.Interval, + TileWidth = options.TileWidth, + TileHeight = options.TileHeight, + ThumbnailCount = existingFiles.Length, + Height = 0, + Bandwidth = 0 + }; + + foreach (var tile in existingFiles) + { + var image = _imageEncoder.GetImageSize(tile); + localTrickplayInfo.Height = Math.Max(localTrickplayInfo.Height, image.Height); + var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tile).Length * 8 / localTrickplayInfo.TileWidth / localTrickplayInfo.TileHeight / (localTrickplayInfo.Interval / 1000)); + localTrickplayInfo.Bandwidth = Math.Max(localTrickplayInfo.Bandwidth, bitrate); + } + + await SaveTrickplayInfo(localTrickplayInfo).ConfigureAwait(false); + + _logger.LogDebug("Imported existing trickplay files for {ItemId}.", video.Id); + return; + } } + // Generate trickplay tiles var mediaStream = mediaSource.VideoStream; var container = mediaSource.Container; @@ -224,7 +323,7 @@ public class TrickplayManager : ITrickplayManager } /// - public TrickplayInfo CreateTiles(List images, int width, TrickplayOptions options, string outputDir) + public TrickplayInfo CreateTiles(IReadOnlyList images, int width, TrickplayOptions options, string outputDir) { if (images.Count == 0) { @@ -264,7 +363,7 @@ public class TrickplayManager : ITrickplayManager var tilePath = Path.Combine(workDir, $"{i}.jpg"); imageOptions.OutputPath = tilePath; - imageOptions.InputPaths = images.GetRange(i * thumbnailsPerTile, Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile))); + imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile))).ToList(); // Generate image and use returned height for tiles info var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null); @@ -289,7 +388,7 @@ public class TrickplayManager : ITrickplayManager Directory.Delete(outputDir, true); } - MoveDirectory(workDir, outputDir); + _fileSystem.MoveDirectory(workDir, outputDir); return trickplayInfo; } @@ -355,6 +454,24 @@ public class TrickplayManager : ITrickplayManager return trickplayResolutions; } + /// + public async Task> GetTrickplayItemsAsync() + { + List trickplayItems; + + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + trickplayItems = await dbContext.TrickplayInfos + .AsNoTracking() + .Select(i => i.ItemId) + .ToListAsync() + .ConfigureAwait(false); + } + + return trickplayItems; + } + /// public async Task SaveTrickplayInfo(TrickplayInfo info) { @@ -392,9 +509,15 @@ public class TrickplayManager : ITrickplayManager } /// - public string GetTrickplayTilePath(BaseItem item, int width, int index) + public async Task GetTrickplayTilePathAsync(BaseItem item, int width, int index, bool saveWithMedia) { - return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg"); + var trickplayResolutions = await GetTrickplayResolutions(item.Id).ConfigureAwait(false); + if (trickplayResolutions is not null && trickplayResolutions.TryGetValue(width, out var trickplayInfo)) + { + return Path.Combine(GetTrickplayDirectory(item, trickplayInfo.TileWidth, trickplayInfo.TileHeight, width, saveWithMedia), index + ".jpg"); + } + + return string.Empty; } /// @@ -470,29 +593,33 @@ public class TrickplayManager : ITrickplayManager return null; } - private string GetTrickplayDirectory(BaseItem item, int? width = null) + /// + public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false) { - var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay"); + var path = saveWithMedia + ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay")) + : Path.Combine(item.GetInternalMetadataPath(), "trickplay"); - return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path; + var subdirectory = string.Format( + CultureInfo.InvariantCulture, + "{0} - {1}x{2}", + width.ToString(CultureInfo.InvariantCulture), + tileWidth.ToString(CultureInfo.InvariantCulture), + tileHeight.ToString(CultureInfo.InvariantCulture)); + + return Path.Combine(path, subdirectory); } - private void MoveDirectory(string source, string destination) + private async Task HasTrickplayResolutionAsync(Guid itemId, int width) { - try + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - Directory.Move(source, destination); - } - catch (IOException) - { - // Cross device move requires a copy - Directory.CreateDirectory(destination); - foreach (string file in Directory.GetFiles(source)) - { - File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true); - } - - Directory.Delete(source, true); + return await dbContext.TrickplayInfos + .AsNoTracking() + .Where(i => i.ItemId.Equals(itemId)) + .AnyAsync(i => i.Width == width) + .ConfigureAwait(false); } } } diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 81fecc9a13..8682f28e04 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -46,6 +46,7 @@ namespace Jellyfin.Server.Migrations typeof(Routines.AddDefaultCastReceivers), typeof(Routines.UpdateDefaultPluginRepository), typeof(Routines.FixAudioData), + typeof(Routines.MoveTrickplayFiles) }; /// diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs new file mode 100644 index 0000000000..e8abee95ad --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs @@ -0,0 +1,73 @@ +using System; +using System.Globalization; +using System.IO; +using DiscUtils; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Trickplay; + +namespace Jellyfin.Server.Migrations.Routines; + +/// +/// Migration to move trickplay files to the new directory. +/// +public class MoveTrickplayFiles : IMigrationRoutine +{ + private readonly ITrickplayManager _trickplayManager; + private readonly IFileSystem _fileSystem; + private readonly ILibraryManager _libraryManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public MoveTrickplayFiles(ITrickplayManager trickplayManager, IFileSystem fileSystem, ILibraryManager libraryManager) + { + _trickplayManager = trickplayManager; + _fileSystem = fileSystem; + _libraryManager = libraryManager; + } + + /// + public Guid Id => new("4EF123D5-8EFF-4B0B-869D-3AED07A60E1B"); + + /// + public string Name => "MoveTrickplayFiles"; + + /// + public bool PerformOnNewInstall => true; + + /// + public void Perform() + { + var trickplayItems = _trickplayManager.GetTrickplayItemsAsync().GetAwaiter().GetResult(); + foreach (var itemId in trickplayItems) + { + var resolutions = _trickplayManager.GetTrickplayResolutions(itemId).GetAwaiter().GetResult(); + var item = _libraryManager.GetItemById(itemId); + if (item is null) + { + continue; + } + + foreach (var resolution in resolutions) + { + var oldPath = GetOldTrickplayDirectory(item, resolution.Key); + var newPath = _trickplayManager.GetTrickplayDirectory(item, resolution.Value.TileWidth, resolution.Value.TileHeight, resolution.Value.Width, false); + if (_fileSystem.DirectoryExists(oldPath)) + { + _fileSystem.MoveDirectory(oldPath, newPath); + } + } + } + } + + private string GetOldTrickplayDirectory(BaseItem item, int? width = null) + { + var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay"); + + return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path; + } +} diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs index 9e91a8bcd7..0bab2a6b9c 100644 --- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs @@ -29,6 +29,7 @@ namespace MediaBrowser.Controller.Providers IsAutomated = copy.IsAutomated; ImageRefreshMode = copy.ImageRefreshMode; ReplaceAllImages = copy.ReplaceAllImages; + RegenerateTrickplay = copy.RegenerateTrickplay; ReplaceImages = copy.ReplaceImages; SearchResult = copy.SearchResult; RemoveOldMetadata = copy.RemoveOldMetadata; @@ -47,6 +48,12 @@ namespace MediaBrowser.Controller.Providers /// public bool ReplaceAllMetadata { get; set; } + /// + /// Gets or sets a value indicating whether all existing trickplay images should be overwritten + /// when paired with MetadataRefreshMode=FullRefresh. + /// + public bool RegenerateTrickplay { get; set; } + public MetadataRefreshMode MetadataRefreshMode { get; set; } public RemoteSearchResult SearchResult { get; set; } diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs index 0c41f30235..bda794aa64 100644 --- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs +++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs @@ -18,9 +18,10 @@ public interface ITrickplayManager /// /// The video. /// Whether or not existing data should be replaced. + /// The library options. /// CancellationToken to use for operation. /// Task. - Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken); + Task RefreshTrickplayDataAsync(Video video, bool replace, LibraryOptions? libraryOptions, CancellationToken cancellationToken); /// /// Creates trickplay tiles out of individual thumbnails. @@ -33,7 +34,7 @@ public interface ITrickplayManager /// /// The output directory will be DELETED and replaced if it already exists. /// - TrickplayInfo CreateTiles(List images, int width, TrickplayOptions options, string outputDir); + TrickplayInfo CreateTiles(IReadOnlyList images, int width, TrickplayOptions options, string outputDir); /// /// Get available trickplay resolutions and corresponding info. @@ -42,6 +43,12 @@ public interface ITrickplayManager /// Map of width resolutions to trickplay tiles info. Task> GetTrickplayResolutions(Guid itemId); + /// + /// Gets the item ids of all items with trickplay info. + /// + /// The list of item ids that have trickplay info. + public Task> GetTrickplayItemsAsync(); + /// /// Saves trickplay info. /// @@ -62,8 +69,29 @@ public interface ITrickplayManager /// The item. /// The width of a single thumbnail. /// The tile's index. + /// Whether or not the tile should be saved next to the media file. /// The absolute path. - string GetTrickplayTilePath(BaseItem item, int width, int index); + Task GetTrickplayTilePathAsync(BaseItem item, int width, int index, bool saveWithMedia); + + /// + /// Gets the path to a trickplay tile image. + /// + /// The item. + /// The amount of images for the tile width. + /// The amount of images for the tile height. + /// The width of a single thumbnail. + /// Whether or not the tile should be saved next to the media file. + /// The absolute path. + string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false); + + /// + /// Migrates trickplay images between local and media directories. + /// + /// The video. + /// The library options. + /// CancellationToken to use for operation. + /// Task. + Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions? libraryOptions, CancellationToken cancellationToken); /// /// Gets the trickplay HLS playlist. diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index b0f5c2a11f..688a6418d0 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -24,6 +24,7 @@ namespace MediaBrowser.Model.Configuration EnablePhotos = true; SaveSubtitlesWithMedia = true; SaveLyricsWithMedia = false; + SaveTrickplayWithMedia = false; PathInfos = Array.Empty(); EnableAutomaticSeriesGrouping = true; SeasonZeroDisplayName = "Specials"; @@ -99,6 +100,9 @@ namespace MediaBrowser.Model.Configuration [DefaultValue(false)] public bool SaveLyricsWithMedia { get; set; } + [DefaultValue(false)] + public bool SaveTrickplayWithMedia { get; set; } + public string[] DisabledLyricFetchers { get; set; } public string[] LyricFetcherOrder { get; set; } diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index ec381d4231..2085328ddc 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -33,6 +33,13 @@ namespace MediaBrowser.Model.IO string MakeAbsolutePath(string folderPath, string filePath); + /// + /// Moves a directory to a new location. + /// + /// Source directory. + /// Destination directory. + void MoveDirectory(string source, string destination); + /// /// Returns a object for the specified file or directory path. /// diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs index 90c2ff8ddf..31c0eeb31e 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs @@ -98,7 +98,8 @@ public class TrickplayImagesTask : IScheduledTask try { - await _trickplayManager.RefreshTrickplayDataAsync(video, false, cancellationToken).ConfigureAwait(false); + var libraryOptions = _libraryManager.GetLibraryOptions(video); + await _trickplayManager.RefreshTrickplayDataAsync(video, false, libraryOptions, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { diff --git a/MediaBrowser.Providers/Trickplay/TrickplayMoveImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayMoveImagesTask.cs new file mode 100644 index 0000000000..c581fd26cb --- /dev/null +++ b/MediaBrowser.Providers/Trickplay/TrickplayMoveImagesTask.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Trickplay; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Providers.Trickplay; + +/// +/// Class TrickplayMoveImagesTask. +/// +public class TrickplayMoveImagesTask : IScheduledTask +{ + private const int QueryPageLimit = 100; + + private readonly ILogger _logger; + private readonly ILibraryManager _libraryManager; + private readonly ILocalizationManager _localization; + private readonly ITrickplayManager _trickplayManager; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The library manager. + /// The localization manager. + /// The trickplay manager. + public TrickplayMoveImagesTask( + ILogger logger, + ILibraryManager libraryManager, + ILocalizationManager localization, + ITrickplayManager trickplayManager) + { + _libraryManager = libraryManager; + _logger = logger; + _localization = localization; + _trickplayManager = trickplayManager; + } + + /// + public string Name => _localization.GetLocalizedString("TaskMoveTrickplayImages"); + + /// + public string Description => _localization.GetLocalizedString("TaskMoveTrickplayImagesDescription"); + + /// + public string Key => "MoveTrickplayImages"; + + /// + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + + /// + public IEnumerable GetDefaultTriggers() => []; + + /// + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + { + var trickplayItems = await _trickplayManager.GetTrickplayItemsAsync().ConfigureAwait(false); + var query = new InternalItemsQuery + { + MediaTypes = [MediaType.Video], + SourceTypes = [SourceType.Library], + IsVirtualItem = false, + IsFolder = false, + Recursive = true, + Limit = QueryPageLimit + }; + + var numberOfVideos = _libraryManager.GetCount(query); + + var startIndex = 0; + var numComplete = 0; + + while (startIndex < numberOfVideos) + { + query.StartIndex = startIndex; + var videos = _libraryManager.GetItemList(query).OfType public class MediaSegmentManager : IMediaSegmentManager { + private readonly ILogger _logger; private readonly IDbContextFactory _dbProvider; + private readonly IMediaSegmentProvider[] _segmentProviders; + private readonly ILibraryManager _libraryManager; /// /// Initializes a new instance of the class. /// + /// Logger. /// EFCore Database factory. - public MediaSegmentManager(IDbContextFactory dbProvider) + /// List of all media segment providers. + /// Library manager. + public MediaSegmentManager( + ILogger logger, + IDbContextFactory dbProvider, + IEnumerable segmentProviders, + ILibraryManager libraryManager) { + _logger = logger; _dbProvider = dbProvider; + + _segmentProviders = segmentProviders + .OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0) + .ToArray(); + _libraryManager = libraryManager; + } + + /// + public async Task RunSegmentPluginProviders(BaseItem baseItem, bool overwrite, CancellationToken cancellationToken) + { + var libraryOptions = _libraryManager.GetLibraryOptions(baseItem); + var providers = _segmentProviders + .Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name))) + .OrderBy(i => + { + var index = libraryOptions.MediaSegmentProvideOrder.IndexOf(i.Name); + return index == -1 ? int.MaxValue : index; + }) + .ToList(); + + _logger.LogInformation("Start media segment extraction from providers with {CountProviders} enabled", providers.Count); + using var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + + if (!overwrite && (await db.MediaSegments.AnyAsync(e => e.ItemId.Equals(baseItem.Id), cancellationToken).ConfigureAwait(false))) + { + _logger.LogInformation("Skip {MediaPath} as it already contains media segments", baseItem.Path); + return; + } + + _logger.LogInformation("Clear existing Segments for {MediaPath}", baseItem.Path); + + await db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + + // no need to recreate the request object every time. + var requestItem = new MediaSegmentGenerationRequest() { ItemId = baseItem.Id }; + + foreach (var provider in providers) + { + if (!await provider.Supports(baseItem).ConfigureAwait(false)) + { + _logger.LogDebug("Media Segment provider {ProviderName} does not support item with path {Path}", provider.Name, baseItem.Path); + continue; + } + + _logger.LogDebug("Run Media Segment provider {ProviderName}", provider.Name); + try + { + var segments = await provider.GetMediaSegments(requestItem, cancellationToken) + .ConfigureAwait(false); + + _logger.LogInformation("Media Segment provider {ProviderName} found {CountSegments} for {MediaPath}", provider.Name, segments.Count, baseItem.Path); + var providerId = GetProviderId(provider.Name); + foreach (var segment in segments) + { + segment.ItemId = baseItem.Id; + await CreateSegmentAsync(segment, providerId).ConfigureAwait(false); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Provider {ProviderName} failed to extract segments from {MediaPath}", provider.Name, baseItem.Path); + } + } } /// @@ -103,4 +186,21 @@ public class MediaSegmentManager : IMediaSegmentManager { return baseItem.MediaType is Data.Enums.MediaType.Video or Data.Enums.MediaType.Audio; } + + /// + public IEnumerable<(string Name, string Id)> GetSupportedProviders(BaseItem item) + { + if (item is not (Video or Audio)) + { + return []; + } + + return _segmentProviders + .Select(p => (p.Name, GetProviderId(p.Name))); + } + + private string GetProviderId(string name) + => name.ToLowerInvariant() + .GetMD5() + .ToString("N", CultureInfo.InvariantCulture); } diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs index 67384f6f64..010d7edb4f 100644 --- a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs +++ b/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; @@ -13,6 +14,15 @@ namespace MediaBrowser.Controller; /// public interface IMediaSegmentManager { + /// + /// Uses all segment providers enabled for the 's library to get the Media Segments. + /// + /// The Item to evaluate. + /// If set, will remove existing segments and replace it with new ones otherwise will check for existing segments and if found any, stops. + /// stop request token. + /// A task that indicates the Operation is finished. + Task RunSegmentPluginProviders(BaseItem baseItem, bool overwrite, CancellationToken cancellationToken); + /// /// Returns if this item supports media segments. /// @@ -50,4 +60,11 @@ public interface IMediaSegmentManager /// True if there are any segments stored for the item, otherwise false. /// TODO: this should be async but as the only caller BaseItem.GetVersionInfo isn't async, this is also not. Venson. bool HasSegments(Guid itemId); + + /// + /// Gets a list of all registered Segment Providers and their IDs. + /// + /// The media item that should be tested for providers. + /// A list of all providers for the tested item. + IEnumerable<(string Name, string Id)> GetSupportedProviders(BaseItem item); } diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs b/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs new file mode 100644 index 0000000000..39bb58bef2 --- /dev/null +++ b/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model; +using MediaBrowser.Model.MediaSegments; + +namespace MediaBrowser.Controller; + +/// +/// Provides methods for Obtaining the Media Segments from an Item. +/// +public interface IMediaSegmentProvider +{ + /// + /// Gets the provider name. + /// + string Name { get; } + + /// + /// Enumerates all Media Segments from an Media Item. + /// + /// Arguments to enumerate MediaSegments. + /// Abort token. + /// A list of all MediaSegments found from this provider. + Task> GetMediaSegments(MediaSegmentGenerationRequest request, CancellationToken cancellationToken); + + /// + /// Should return support state for the given item. + /// + /// The base item to extract segments from. + /// True if item is supported, otherwise false. + ValueTask Supports(BaseItem item); +} diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index 688a6418d0..90ac377f47 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -11,6 +11,8 @@ namespace MediaBrowser.Model.Configuration { TypeOptions = Array.Empty(); DisabledSubtitleFetchers = Array.Empty(); + DisabledMediaSegmentProviders = Array.Empty(); + MediaSegmentProvideOrder = Array.Empty(); SubtitleFetcherOrder = Array.Empty(); DisabledLocalMetadataReaders = Array.Empty(); DisabledLyricFetchers = Array.Empty(); @@ -87,6 +89,10 @@ namespace MediaBrowser.Model.Configuration public string[] SubtitleFetcherOrder { get; set; } + public string[] DisabledMediaSegmentProviders { get; set; } + + public string[] MediaSegmentProvideOrder { get; set; } + public bool SkipSubtitlesIfEmbeddedSubtitlesPresent { get; set; } public bool SkipSubtitlesIfAudioTrackMatches { get; set; } diff --git a/MediaBrowser.Model/Configuration/MetadataPluginType.cs b/MediaBrowser.Model/Configuration/MetadataPluginType.cs index ef303726d1..670d6e3837 100644 --- a/MediaBrowser.Model/Configuration/MetadataPluginType.cs +++ b/MediaBrowser.Model/Configuration/MetadataPluginType.cs @@ -14,6 +14,7 @@ namespace MediaBrowser.Model.Configuration MetadataFetcher, MetadataSaver, SubtitleFetcher, - LyricFetcher + LyricFetcher, + MediaSegmentProvider } } diff --git a/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs b/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs new file mode 100644 index 0000000000..8c1f44de8c --- /dev/null +++ b/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs @@ -0,0 +1,14 @@ +using System; + +namespace MediaBrowser.Model; + +/// +/// Model containing the arguments for enumerating the requested media item. +/// +public record MediaSegmentGenerationRequest +{ + /// + /// Gets the Id to the BaseItem the segments should be extracted from. + /// + public Guid ItemId { get; init; } +} diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 60d89a51b7..81a9af68be 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -62,7 +62,7 @@ namespace MediaBrowser.Providers.Manager private readonly CancellationTokenSource _disposeCancellationTokenSource = new(); private readonly PriorityQueue<(Guid ItemId, MetadataRefreshOptions RefreshOptions), RefreshPriority> _refreshQueue = new(); private readonly IMemoryCache _memoryCache; - + private readonly IMediaSegmentManager _mediaSegmentManager; private readonly AsyncKeyedLocker _imageSaveLock = new(o => { o.PoolSize = 20; @@ -92,6 +92,7 @@ namespace MediaBrowser.Providers.Manager /// The BaseItem manager. /// The lyric manager. /// The memory cache. + /// The media segment manager. public ProviderManager( IHttpClientFactory httpClientFactory, ISubtitleManager subtitleManager, @@ -103,7 +104,8 @@ namespace MediaBrowser.Providers.Manager ILibraryManager libraryManager, IBaseItemManager baseItemManager, ILyricManager lyricManager, - IMemoryCache memoryCache) + IMemoryCache memoryCache, + IMediaSegmentManager mediaSegmentManager) { _logger = logger; _httpClientFactory = httpClientFactory; @@ -116,6 +118,7 @@ namespace MediaBrowser.Providers.Manager _baseItemManager = baseItemManager; _lyricManager = lyricManager; _memoryCache = memoryCache; + _mediaSegmentManager = mediaSegmentManager; } /// @@ -572,6 +575,14 @@ namespace MediaBrowser.Providers.Manager Type = MetadataPluginType.LyricFetcher })); + // Media segment providers + var mediaSegmentProviders = _mediaSegmentManager.GetSupportedProviders(dummy); + pluginList.AddRange(mediaSegmentProviders.Select(i => new MetadataPlugin + { + Name = i.Name, + Type = MetadataPluginType.MediaSegmentProvider + })); + summary.Plugins = pluginList.ToArray(); var supportedImageTypes = imageProviders.OfType() diff --git a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs index cced2b1e26..c227883b50 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs @@ -574,7 +574,8 @@ namespace Jellyfin.Providers.Tests.Manager libraryManager.Object, baseItemManager!, Mock.Of(), - Mock.Of()); + Mock.Of(), + Mock.Of()); return providerManager; } From c7bb2fe137aea5b819a86eb50bd51f094135c546 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sat, 7 Sep 2024 18:08:41 -0400 Subject: [PATCH 010/159] Backport pull request #12531 from jellyfin/release-10.9.z Don't apply chapter image settings to music Original-merge: 2fe13f54eaf87eefefd27f4ccb2ace1371f5e886 Merged-by: nielsvanvelzen Backported-by: Joshua M. Boniface --- .../Encoder/MediaEncoder.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 764230febe..7091d5153e 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -598,7 +598,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { try { - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, targetFormat, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, targetFormat, false, cancellationToken).ConfigureAwait(false); } catch (ArgumentException) { @@ -610,7 +610,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, targetFormat, cancellationToken).ConfigureAwait(false); + return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, targetFormat, isAudio, cancellationToken).ConfigureAwait(false); } private string GetImageResolutionParameter() @@ -636,7 +636,17 @@ namespace MediaBrowser.MediaEncoding.Encoder return imageResolutionParameter; } - private async Task ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, ImageFormat? targetFormat, CancellationToken cancellationToken) + private async Task ExtractImageInternal( + string inputPath, + string container, + MediaStream videoStream, + int? imageStreamIndex, + Video3DFormat? threedFormat, + TimeSpan? offset, + bool useIFrame, + ImageFormat? targetFormat, + bool isAudio, + CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrEmpty(inputPath); @@ -701,7 +711,7 @@ namespace MediaBrowser.MediaEncoding.Encoder var vf = string.Join(',', filters); var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty; - var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 -vf {2}{5} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads, GetImageResolutionParameter()); + var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 -vf {2}{5} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads, isAudio ? string.Empty : GetImageResolutionParameter()); if (offset.HasValue) { From 7631956451af5927c1c9850eb4e106dc4d0cdde1 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sat, 7 Sep 2024 18:09:52 -0400 Subject: [PATCH 011/159] Backport pull request #12550 from jellyfin/release-10.9.z Create and use FormattingStreamWriter Original-merge: cd2f2ca17800f71c8d94a6e043b49b7c4200e254 Merged-by: Bond-009 Backported-by: Joshua M. Boniface --- .../Entities/TV/Episode.cs | 5 +-- .../Entities/UserViewBuilder.cs | 2 - .../Encoder/MediaEncoder.cs | 2 +- .../FormattingStreamWriter.cs | 38 +++++++++++++++++++ .../FormattingStreamWriterTests.cs | 23 +++++++++++ 5 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 src/Jellyfin.Extensions/FormattingStreamWriter.cs create mode 100644 tests/Jellyfin.Extensions.Tests/FormattingStreamWriterTests.cs diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 5c54f014cf..46bad3f3be 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -180,10 +180,7 @@ namespace MediaBrowser.Controller.Entities.TV } public string FindSeriesPresentationUniqueKey() - { - var series = Series; - return series is null ? null : series.PresentationUniqueKey; - } + => Series?.PresentationUniqueKey; public string FindSeasonName() { diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 2fda7ee6f7..420349f35c 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -430,8 +430,6 @@ namespace MediaBrowser.Controller.Entities InternalItemsQuery query, ILibraryManager libraryManager) { - var user = query.User; - // This must be the last filter if (!query.AdjacentTo.IsNullOrEmpty()) { diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 7091d5153e..5e2e90c68b 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -1206,7 +1206,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // Generate concat configuration entries for each file and write to file Directory.CreateDirectory(Path.GetDirectoryName(concatFilePath)); - using StreamWriter sw = new StreamWriter(concatFilePath); + using StreamWriter sw = new FormattingStreamWriter(concatFilePath, CultureInfo.InvariantCulture); foreach (var path in files) { var mediaInfoResult = GetMediaInfo( diff --git a/src/Jellyfin.Extensions/FormattingStreamWriter.cs b/src/Jellyfin.Extensions/FormattingStreamWriter.cs new file mode 100644 index 0000000000..40e3c5a68f --- /dev/null +++ b/src/Jellyfin.Extensions/FormattingStreamWriter.cs @@ -0,0 +1,38 @@ +using System; +using System.IO; + +namespace Jellyfin.Extensions; + +/// +/// A custom StreamWriter which supports setting a IFormatProvider. +/// +public class FormattingStreamWriter : StreamWriter +{ + private readonly IFormatProvider _formatProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The stream to write to. + /// The format provider to use. + public FormattingStreamWriter(Stream stream, IFormatProvider formatProvider) + : base(stream) + { + _formatProvider = formatProvider; + } + + /// + /// Initializes a new instance of the class. + /// + /// The complete file path to write to. + /// The format provider to use. + public FormattingStreamWriter(string path, IFormatProvider formatProvider) + : base(path) + { + _formatProvider = formatProvider; + } + + /// + public override IFormatProvider FormatProvider + => _formatProvider; +} diff --git a/tests/Jellyfin.Extensions.Tests/FormattingStreamWriterTests.cs b/tests/Jellyfin.Extensions.Tests/FormattingStreamWriterTests.cs new file mode 100644 index 0000000000..06e3c27213 --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/FormattingStreamWriterTests.cs @@ -0,0 +1,23 @@ +using System.Globalization; +using System.IO; +using System.Text; +using System.Threading; +using Xunit; + +namespace Jellyfin.Extensions.Tests; + +public static class FormattingStreamWriterTests +{ + [Fact] + public static void Shuffle_Valid_Correct() + { + Thread.CurrentThread.CurrentCulture = new CultureInfo("de-DE", false); + using (var ms = new MemoryStream()) + using (var txt = new FormattingStreamWriter(ms, CultureInfo.InvariantCulture)) + { + txt.Write("{0}", 3.14159); + txt.Close(); + Assert.Equal("3.14159", Encoding.UTF8.GetString(ms.ToArray())); + } + } +} From e10b986ea0e8aea98fd83d3d8d30c5c2ac385f73 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sat, 7 Sep 2024 18:09:53 -0400 Subject: [PATCH 012/159] Backport pull request #12558 from jellyfin/release-10.9.z Fix alt version name generation Original-merge: 70f4f2e8c2378f9a219c840ac23d0bcd2638c966 Merged-by: Bond-009 Backported-by: Joshua M. Boniface --- MediaBrowser.Controller/Entities/BaseItem.cs | 17 ++++++----- .../Entities/BaseItemTests.cs | 29 +++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 125f8f225c..05a7b7896f 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1185,28 +1185,29 @@ namespace MediaBrowser.Controller.Entities return info; } - private string GetMediaSourceName(BaseItem item) + internal string GetMediaSourceName(BaseItem item) { var terms = new List(); var path = item.Path; if (item.IsFileProtocol && !string.IsNullOrEmpty(path)) { + var displayName = System.IO.Path.GetFileNameWithoutExtension(path); if (HasLocalAlternateVersions) { - var displayName = System.IO.Path.GetFileNameWithoutExtension(path) - .Replace(System.IO.Path.GetFileName(ContainingFolderPath), string.Empty, StringComparison.OrdinalIgnoreCase) - .TrimStart(new char[] { ' ', '-' }); - - if (!string.IsNullOrEmpty(displayName)) + var containingFolderName = System.IO.Path.GetFileName(ContainingFolderPath); + if (displayName.Length > containingFolderName.Length && displayName.StartsWith(containingFolderName, StringComparison.OrdinalIgnoreCase)) { - terms.Add(displayName); + var name = displayName.AsSpan(containingFolderName.Length).TrimStart([' ', '-']); + if (!name.IsWhiteSpace()) + { + terms.Add(name.ToString()); + } } } if (terms.Count == 0) { - var displayName = System.IO.Path.GetFileNameWithoutExtension(path); terms.Add(displayName); } } diff --git a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs index f3ada59dbc..6171f12e47 100644 --- a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs +++ b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs @@ -1,4 +1,7 @@ using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.MediaInfo; +using Moq; using Xunit; namespace Jellyfin.Controller.Tests.Entities; @@ -14,4 +17,30 @@ public class BaseItemTests [InlineData("1test 2", "0000000001test 0000000002")] public void BaseItem_ModifySortChunks_Valid(string input, string expected) => Assert.Equal(expected, BaseItem.ModifySortChunks(input)); + + [Theory] + [InlineData("/Movies/Ted/Ted.mp4", "/Movies/Ted/Ted - Unrated Edition.mp4", "Ted", "Unrated Edition")] + [InlineData("/Movies/Deadpool 2 (2018)/Deadpool 2 (2018).mkv", "/Movies/Deadpool 2 (2018)/Deadpool 2 (2018) - Super Duper Cut.mkv", "Deadpool 2 (2018)", "Super Duper Cut")] + public void GetMediaSourceName_Valid(string primaryPath, string altPath, string name, string altName) + { + var mediaSourceManager = new Mock(); + mediaSourceManager.Setup(x => x.GetPathProtocol(It.IsAny())) + .Returns((string x) => MediaProtocol.File); + BaseItem.MediaSourceManager = mediaSourceManager.Object; + + var video = new Video() + { + Path = primaryPath + }; + + var videoAlt = new Video() + { + Path = altPath, + }; + + video.LocalAlternateVersions = [videoAlt.Path]; + + Assert.Equal(name, video.GetMediaSourceName(video)); + Assert.Equal(altName, video.GetMediaSourceName(videoAlt)); + } } From 84b20afe1fc6cb86d9ff8694b3e4524461dea588 Mon Sep 17 00:00:00 2001 From: dmitrylyzo <56478732+dmitrylyzo@users.noreply.github.com> Date: Sat, 7 Sep 2024 18:09:54 -0400 Subject: [PATCH 013/159] Backport pull request #12575 from jellyfin/release-10.9.z Fix subtitle and attachment extraction when input path contains quotes Original-merge: 3c3ebe834462bbb7630ca4fc0c106cc51ca80e50 Merged-by: Bond-009 Backported-by: Joshua M. Boniface --- .../Attachments/AttachmentExtractor.cs | 4 ++-- MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index 7e307286a9..431fc0b178 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -284,7 +284,7 @@ namespace MediaBrowser.MediaEncoding.Attachments if (extractableAttachmentIds.Count > 0) { - await CacheAllAttachmentsInternal(mediaPath, inputFile, mediaSource, extractableAttachmentIds, cancellationToken).ConfigureAwait(false); + await CacheAllAttachmentsInternal(mediaPath, _mediaEncoder.GetInputArgument(inputFile, mediaSource), mediaSource, extractableAttachmentIds, cancellationToken).ConfigureAwait(false); } } catch (Exception ex) @@ -323,7 +323,7 @@ namespace MediaBrowser.MediaEncoding.Attachments processArgs += string.Format( CultureInfo.InvariantCulture, - " -i \"{0}\" -t 0 -f null null", + " -i {0} -t 0 -f null null", inputFile); int exitCode; diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 9ecbfa9cf5..dbb4b823e3 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -529,11 +529,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles List subtitleStreams, CancellationToken cancellationToken) { - var inputPath = mediaSource.Path; + var inputPath = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource); var outputPaths = new List(); var args = string.Format( CultureInfo.InvariantCulture, - "-i \"{0}\" -copyts", + "-i {0} -copyts", inputPath); foreach (var subtitleStream in subtitleStreams) @@ -704,7 +704,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var processArgs = string.Format( CultureInfo.InvariantCulture, - "-i \"{0}\" -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"", + "-i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"", inputPath, subtitleStreamIndex, outputCodec, From b4f71859d939c5c0fa0a155f712d25cba0c20b8f Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 8 Sep 2024 11:08:54 +0800 Subject: [PATCH 014/159] Make Live TV compatibility profiles customizable (#12529) --- Jellyfin.Api/Helpers/StreamingHelpers.cs | 25 +++++-------------- MediaBrowser.Model/Dlna/StreamBuilder.cs | 1 + MediaBrowser.Model/Dto/MediaSourceInfo.cs | 7 ++++++ MediaBrowser.Model/LiveTv/TunerHostInfo.cs | 9 +++++++ .../TunerHosts/HdHomerun/HdHomerunHost.cs | 2 ++ .../TunerHosts/M3UTunerHost.cs | 6 +++-- 6 files changed, 29 insertions(+), 21 deletions(-) diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 535ef27c3a..3cc6a393bc 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -142,28 +142,15 @@ public static class StreamingHelpers } else { - // Enforce more restrictive transcoding profile for LiveTV due to compatability reasons - // Cap the MaxStreamingBitrate to 30Mbps, because we are unable to reliably probe source bitrate, - // which will cause the client to request extremely high bitrate that may fail the player/encoder - streamingRequest.VideoBitRate = streamingRequest.VideoBitRate > 30000000 ? 30000000 : streamingRequest.VideoBitRate; - - if (streamingRequest.SegmentContainer is not null) - { - // Remove all fmp4 transcoding profiles, because it causes playback error and/or A/V sync issues - // Notably: Some channels won't play on FireFox and LG webOS - // Some channels from HDHomerun will experience A/V sync issues - streamingRequest.SegmentContainer = "ts"; - streamingRequest.VideoCodec = "h264"; - streamingRequest.AudioCodec = "aac"; - state.SupportedVideoCodecs = ["h264"]; - state.Request.VideoCodec = "h264"; - state.SupportedAudioCodecs = ["aac"]; - state.Request.AudioCodec = "aac"; - } - var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false); mediaSource = liveStreamInfo.Item1; state.DirectStreamProvider = liveStreamInfo.Item2; + + // Cap the max bitrate when it is too high. This is usually due to ffmpeg is unable to probe the source liveTV streams' bitrate. + if (mediaSource.FallbackMaxStreamingBitrate is not null && streamingRequest.VideoBitRate is not null) + { + streamingRequest.VideoBitRate = Math.Min(streamingRequest.VideoBitRate.Value, mediaSource.FallbackMaxStreamingBitrate.Value); + } } var encodingOptions = serverConfigurationManager.GetEncodingOptions(); diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index ad00149e0a..c18becf72a 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -805,6 +805,7 @@ namespace MediaBrowser.Model.Dlna } var transcodingProfiles = options.Profile.TranscodingProfiles + .Where(i => !item.UseMostCompatibleTranscodingProfile || string.Equals(i.Container, "ts", StringComparison.OrdinalIgnoreCase)) .Where(i => i.Type == playlistItem.MediaType && i.Context == options.Context); if (options.AllowVideoStreamCopy) diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs index 1c6037325b..eff2e09da1 100644 --- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs +++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; using MediaBrowser.Model.Dlna; @@ -24,6 +25,7 @@ namespace MediaBrowser.Model.Dto SupportsDirectStream = true; SupportsDirectPlay = true; SupportsProbing = true; + UseMostCompatibleTranscodingProfile = false; } public MediaProtocol Protocol { get; set; } @@ -70,6 +72,9 @@ namespace MediaBrowser.Model.Dto public bool IsInfiniteStream { get; set; } + [DefaultValue(false)] + public bool UseMostCompatibleTranscodingProfile { get; set; } + public bool RequiresOpening { get; set; } public string OpenToken { get; set; } @@ -98,6 +103,8 @@ namespace MediaBrowser.Model.Dto public int? Bitrate { get; set; } + public int? FallbackMaxStreamingBitrate { get; set; } + public TransportStreamTimestamp? Timestamp { get; set; } public Dictionary RequiredHttpHeaders { get; set; } diff --git a/MediaBrowser.Model/LiveTv/TunerHostInfo.cs b/MediaBrowser.Model/LiveTv/TunerHostInfo.cs index a832169c2a..a355387b1a 100644 --- a/MediaBrowser.Model/LiveTv/TunerHostInfo.cs +++ b/MediaBrowser.Model/LiveTv/TunerHostInfo.cs @@ -9,6 +9,9 @@ namespace MediaBrowser.Model.LiveTv { AllowHWTranscoding = true; IgnoreDts = true; + AllowStreamSharing = true; + AllowFmp4TranscodingContainer = false; + FallbackMaxStreamingBitrate = 30000000; } public string Id { get; set; } @@ -25,6 +28,12 @@ namespace MediaBrowser.Model.LiveTv public bool AllowHWTranscoding { get; set; } + public bool AllowFmp4TranscodingContainer { get; set; } + + public bool AllowStreamSharing { get; set; } + + public int FallbackMaxStreamingBitrate { get; set; } + public bool EnableStreamLooping { get; set; } public string Source { get; set; } diff --git a/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index fef84dd000..e1f87a7bd4 100644 --- a/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -331,6 +331,8 @@ namespace Jellyfin.LiveTv.TunerHosts.HdHomerun SupportsTranscoding = true, IsInfiniteStream = true, IgnoreDts = true, + UseMostCompatibleTranscodingProfile = true, // All HDHR tuners require this + FallbackMaxStreamingBitrate = info.FallbackMaxStreamingBitrate, // IgnoreIndex = true, // ReadAtNativeFramerate = true }; diff --git a/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs index 365f0188df..be81171a03 100644 --- a/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs @@ -94,7 +94,7 @@ namespace Jellyfin.LiveTv.TunerHosts var mediaSource = sources[0]; - if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping) + if (tunerHost.AllowStreamSharing && mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping) { var extension = Path.GetExtension(new UriBuilder(mediaSource.Path).Path); @@ -200,7 +200,9 @@ namespace Jellyfin.LiveTv.TunerHosts SupportsDirectPlay = supportsDirectPlay, SupportsDirectStream = supportsDirectStream, - RequiredHttpHeaders = httpHeaders + RequiredHttpHeaders = httpHeaders, + UseMostCompatibleTranscodingProfile = !info.AllowFmp4TranscodingContainer, + FallbackMaxStreamingBitrate = info.FallbackMaxStreamingBitrate }; mediaSource.InferTotalBitrate(); From c6de7225b9f493bb18f5e7362d785ac8a71e5f32 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 8 Sep 2024 11:10:59 +0800 Subject: [PATCH 015/159] Add non-standard multi-value audio tag support (#12385) --- .../Configuration/LibraryOptions.cs | 18 ++++++ .../MediaInfo/AudioFileProber.cs | 61 ++++++++++++++++++- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index 90ac377f47..04283cc9ec 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -7,6 +7,8 @@ namespace MediaBrowser.Model.Configuration { public class LibraryOptions { + private static readonly char[] _defaultTagDelimiters = ['/', '|', ';', '\\']; + public LibraryOptions() { TypeOptions = Array.Empty(); @@ -30,6 +32,11 @@ namespace MediaBrowser.Model.Configuration PathInfos = Array.Empty(); EnableAutomaticSeriesGrouping = true; SeasonZeroDisplayName = "Specials"; + + PreferNonstandardArtistsTag = false; + UseCustomTagDelimiters = false; + CustomTagDelimiters = _defaultTagDelimiters; + DelimiterWhitelist = Array.Empty(); } public bool Enabled { get; set; } = true; @@ -113,6 +120,17 @@ namespace MediaBrowser.Model.Configuration public string[] LyricFetcherOrder { get; set; } + [DefaultValue(false)] + public bool PreferNonstandardArtistsTag { get; set; } + + [DefaultValue(false)] + public bool UseCustomTagDelimiters { get; set; } + + [DefaultValue(typeof(LibraryOptions), nameof(_defaultTagDelimiters))] + public char[] CustomTagDelimiters { get; set; } + + public string[] DelimiterWhitelist { get; set; } + public bool AutomaticallyAddToCollection { get; set; } public EmbeddedSubtitleOptions AllowEmbeddedSubtitles { get; set; } diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 7e0773b6d3..51ac558b86 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using ATL; using Jellyfin.Data.Enums; -using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -158,6 +157,7 @@ namespace MediaBrowser.Providers.MediaInfo /// Whether to extract embedded lyrics to lrc file. private async Task FetchDataFromTags(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, MetadataRefreshOptions options, bool tryExtractEmbeddedLyrics) { + var libraryOptions = _libraryManager.GetLibraryOptions(audio); Track track = new Track(audio.Path); // ATL will fall back to filename as title when it does not understand the metadata @@ -175,6 +175,12 @@ namespace MediaBrowser.Providers.MediaInfo { var people = new List(); var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? mediaInfo.AlbumArtists : track.AlbumArtist.Split(InternalValueSeparator); + + if (libraryOptions.UseCustomTagDelimiters) + { + albumArtists = albumArtists.SelectMany(a => SplitWithCustomDelimiter(a, libraryOptions.CustomTagDelimiters, libraryOptions.DelimiterWhitelist)).ToArray(); + } + foreach (var albumArtist in albumArtists) { if (!string.IsNullOrEmpty(albumArtist)) @@ -187,7 +193,26 @@ namespace MediaBrowser.Providers.MediaInfo } } - var performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split(InternalValueSeparator); + string[]? performers = null; + if (libraryOptions.PreferNonstandardArtistsTag) + { + track.AdditionalFields.TryGetValue("ARTISTS", out var artistsTagString); + if (artistsTagString is not null) + { + performers = artistsTagString.Split(InternalValueSeparator); + } + } + + if (performers is null || performers.Length == 0) + { + performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split(InternalValueSeparator); + } + + if (libraryOptions.UseCustomTagDelimiters) + { + performers = performers.SelectMany(p => SplitWithCustomDelimiter(p, libraryOptions.CustomTagDelimiters, libraryOptions.DelimiterWhitelist)).ToArray(); + } + foreach (var performer in performers) { if (!string.IsNullOrEmpty(performer)) @@ -285,6 +310,12 @@ namespace MediaBrowser.Providers.MediaInfo if (!audio.LockedFields.Contains(MetadataField.Genres)) { var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + + if (libraryOptions.UseCustomTagDelimiters) + { + genres = genres.SelectMany(g => SplitWithCustomDelimiter(g, libraryOptions.CustomTagDelimiters, libraryOptions.DelimiterWhitelist)).ToArray(); + } + audio.Genres = options.ReplaceAllMetadata || audio.Genres == null || audio.Genres.Length == 0 ? genres : audio.Genres; @@ -379,5 +410,31 @@ namespace MediaBrowser.Providers.MediaInfo currentStreams.Add(externalLyricFiles[0]); } } + + private List SplitWithCustomDelimiter(string val, char[] tagDelimiters, string[] whitelist) + { + var items = new List(); + var temp = val; + foreach (var whitelistItem in whitelist) + { + if (string.IsNullOrWhiteSpace(whitelistItem)) + { + continue; + } + + var originalTemp = temp; + temp = temp.Replace(whitelistItem, string.Empty, StringComparison.OrdinalIgnoreCase); + + if (!string.Equals(temp, originalTemp, StringComparison.OrdinalIgnoreCase)) + { + items.Add(whitelistItem); + } + } + + var items2 = temp.Split(tagDelimiters, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).DistinctNames(); + items.AddRange(items2); + + return items; + } } } From 97ba12b8ef97763b6f342d745d135fb159311dfd Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 8 Sep 2024 11:45:38 +0800 Subject: [PATCH 016/159] Fix FormattingStreamWriter type Signed-off-by: gnattu --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 5e2e90c68b..a6a443f3dd 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -1206,7 +1206,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // Generate concat configuration entries for each file and write to file Directory.CreateDirectory(Path.GetDirectoryName(concatFilePath)); - using StreamWriter sw = new FormattingStreamWriter(concatFilePath, CultureInfo.InvariantCulture); + using var sw = new FormattingStreamWriter(concatFilePath, CultureInfo.InvariantCulture); foreach (var path in files) { var mediaInfoResult = GetMediaInfo( From a1b84d2dead253cc4c08d03106b9369eaeb73aa9 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 8 Sep 2024 15:05:55 +0800 Subject: [PATCH 017/159] Fix trickplay migration The auto import generated by IDE used wrong namespace Signed-off-by: gnattu --- Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs index e8abee95ad..301541b6ce 100644 --- a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs +++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs @@ -1,10 +1,10 @@ using System; using System.Globalization; using System.IO; -using DiscUtils; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Trickplay; +using MediaBrowser.Model.IO; namespace Jellyfin.Server.Migrations.Routines; From 40c5dc92c01354d575cd394a26150d861f4457f7 Mon Sep 17 00:00:00 2001 From: TimGels <43609220+TimGels@users.noreply.github.com> Date: Sun, 8 Sep 2024 15:27:02 +0200 Subject: [PATCH 018/159] Update issue template version from 10.9.10 to 10.9.11 --- .github/ISSUE_TEMPLATE/issue report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml index cfb5a6ec25..b522412088 100644 --- a/.github/ISSUE_TEMPLATE/issue report.yml +++ b/.github/ISSUE_TEMPLATE/issue report.yml @@ -86,7 +86,7 @@ body: label: Jellyfin Server version description: What version of Jellyfin are you using? options: - - 10.9.10+ + - 10.9.11+ - Master - Unstable - Older* From cfb19fa9fcadae91174ef13d4844e22e51f025a0 Mon Sep 17 00:00:00 2001 From: bene toffix Date: Sat, 7 Sep 2024 14:13:29 +0000 Subject: [PATCH 019/159] Translated using Weblate (Catalan) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ca/ --- Emby.Server.Implementations/Localization/Core/ca.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 2998489b57..6b3b78fa12 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -130,5 +130,7 @@ "TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.", "TaskCleanCollectionsAndPlaylists": "Neteja col·leccions i llistes de reproducció", "TaskAudioNormalization": "Normalització d'Àudio", - "TaskAudioNormalizationDescription": "Escaneja arxius per dades de normalització d'àudio." + "TaskAudioNormalizationDescription": "Escaneja arxius per dades de normalització d'àudio.", + "TaskDownloadMissingLyricsDescription": "Baixar lletres de les cançons", + "TaskDownloadMissingLyrics": "Baixar lletres que falten" } From 57b17b174f8c3677d33587373193cacaed54354b Mon Sep 17 00:00:00 2001 From: fabriciodeuner Date: Sat, 7 Sep 2024 17:21:29 +0000 Subject: [PATCH 020/159] Translated using Weblate (Portuguese (Brazil)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_BR/ --- Emby.Server.Implementations/Localization/Core/pt-BR.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index d9867f5e05..0c9f4c1710 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -130,5 +130,7 @@ "TaskCleanCollectionsAndPlaylists": "Limpe coleções e playlists", "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e playlists que não existem mais.", "TaskAudioNormalization": "Normalização de áudio", - "TaskAudioNormalizationDescription": "Examina os ficheiros em busca de dados de normalização de áudio." + "TaskAudioNormalizationDescription": "Examina os ficheiros em busca de dados de normalização de áudio.", + "TaskDownloadMissingLyricsDescription": "Baixar letras para músicas", + "TaskDownloadMissingLyrics": "Baixar letra faltante" } From ae1dd5b1fcd1b9bc69c81591ca19c9a0a8e5589c Mon Sep 17 00:00:00 2001 From: fabriciodeuner Date: Sat, 7 Sep 2024 16:32:38 +0000 Subject: [PATCH 021/159] Translated using Weblate (Portuguese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/ --- Emby.Server.Implementations/Localization/Core/pt.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index ff9a0d4f42..d157547def 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -129,5 +129,7 @@ "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.", "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução", "TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.", - "TaskAudioNormalization": "Normalização de áudio" + "TaskAudioNormalization": "Normalização de áudio", + "TaskDownloadMissingLyrics": "Baixar letras faltantes", + "TaskDownloadMissingLyricsDescription": "Baixa letras para músicas" } From 54f663b0f3c4a9cc5a4f44d1afcb6e1de03c0503 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com> Date: Mon, 9 Sep 2024 16:51:28 +0300 Subject: [PATCH 022/159] Extract condition from Where clause to eliminate extra filtering (#12614) --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index c18becf72a..490ae4e629 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -805,9 +805,13 @@ namespace MediaBrowser.Model.Dlna } var transcodingProfiles = options.Profile.TranscodingProfiles - .Where(i => !item.UseMostCompatibleTranscodingProfile || string.Equals(i.Container, "ts", StringComparison.OrdinalIgnoreCase)) .Where(i => i.Type == playlistItem.MediaType && i.Context == options.Context); + if (item.UseMostCompatibleTranscodingProfile) + { + transcodingProfiles = transcodingProfiles.Where(i => string.Equals(i.Container, "ts", StringComparison.OrdinalIgnoreCase)); + } + if (options.AllowVideoStreamCopy) { // prefer direct copy profile From 0d85af019c6ebc855ab2d8e689abe72b995225b4 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Mon, 9 Sep 2024 16:43:37 +0200 Subject: [PATCH 023/159] Use enums for encoding options (#12561) --- .../Controllers/DynamicHlsController.cs | 4 +- Jellyfin.Api/Controllers/VideosController.cs | 2 +- Jellyfin.Server/Migrations/MigrationRunner.cs | 3 +- .../CreateNetworkConfiguration.cs | 1 - .../MigrateEncodingOptions.cs | 245 ++++++++ .../MigrateMusicBrainzTimeout.cs | 10 +- .../MigrateNetworkConfiguration.cs | 87 +-- .../MediaEncoding/EncodingHelper.cs | 594 ++++++++---------- .../Encoder/MediaEncoder.cs | 17 +- .../Transcoding/TranscodeManager.cs | 7 +- .../Configuration/EncodingOptions.cs | 26 +- .../Entities/DeinterlaceMethod.cs | 19 + MediaBrowser.Model/Entities/EncoderPreset.cs | 64 ++ .../Entities/HardwareAccelerationType.cs | 49 ++ .../Entities/TonemappingAlgorithm.cs | 49 ++ .../Entities/TonemappingMode.cs | 34 + .../Entities/TonemappingRange.cs | 24 + .../Session/HardwareEncodingType.cs | 43 -- MediaBrowser.Model/Session/TranscodingInfo.cs | 78 ++- .../Playlist/DynamicHlsPlaylistGenerator.cs | 4 +- 20 files changed, 884 insertions(+), 476 deletions(-) create mode 100644 Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs create mode 100644 MediaBrowser.Model/Entities/DeinterlaceMethod.cs create mode 100644 MediaBrowser.Model/Entities/EncoderPreset.cs create mode 100644 MediaBrowser.Model/Entities/HardwareAccelerationType.cs create mode 100644 MediaBrowser.Model/Entities/TonemappingAlgorithm.cs create mode 100644 MediaBrowser.Model/Entities/TonemappingMode.cs create mode 100644 MediaBrowser.Model/Entities/TonemappingRange.cs delete mode 100644 MediaBrowser.Model/Session/HardwareEncodingType.cs diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index b9ef189e98..db1d866985 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -40,8 +40,8 @@ namespace Jellyfin.Api.Controllers; [Authorize] public class DynamicHlsController : BaseJellyfinApiController { - private const string DefaultVodEncoderPreset = "veryfast"; - private const string DefaultEventEncoderPreset = "superfast"; + private const EncoderPreset DefaultVodEncoderPreset = EncoderPreset.veryfast; + private const EncoderPreset DefaultEventEncoderPreset = EncoderPreset.superfast; private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls; private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0); diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index effe7b021b..8348fd937d 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -482,7 +482,7 @@ public class VideosController : BaseJellyfinApiController // Need to start ffmpeg (because media can't be returned directly) var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); - var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, "superfast"); + var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, EncoderPreset.superfast); return await FileStreamResponseHelpers.GetTranscodedFile( state, isHeadRequest, diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 8682f28e04..9d4441ac39 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -23,7 +23,8 @@ namespace Jellyfin.Server.Migrations { typeof(PreStartupRoutines.CreateNetworkConfiguration), typeof(PreStartupRoutines.MigrateMusicBrainzTimeout), - typeof(PreStartupRoutines.MigrateNetworkConfiguration) + typeof(PreStartupRoutines.MigrateNetworkConfiguration), + typeof(PreStartupRoutines.MigrateEncodingOptions) }; /// diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs index 139a6ec640..8462d0a8c9 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs @@ -132,5 +132,4 @@ public class CreateNetworkConfiguration : IMigrationRoutine public string[] KnownProxies { get; set; } = Array.Empty(); } -#pragma warning restore } diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs new file mode 100644 index 0000000000..61f5620dc0 --- /dev/null +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs @@ -0,0 +1,245 @@ +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; +using Emby.Server.Implementations; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.PreStartupRoutines; + +/// +public class MigrateEncodingOptions : IMigrationRoutine +{ + private readonly ServerApplicationPaths _applicationPaths; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// An instance of the interface. + public MigrateEncodingOptions(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory) + { + _applicationPaths = applicationPaths; + _logger = loggerFactory.CreateLogger(); + } + + /// + public Guid Id => Guid.Parse("A8E61960-7726-4450-8F3D-82C12DAABBCB"); + + /// + public string Name => nameof(MigrateEncodingOptions); + + /// + public bool PerformOnNewInstall => false; + + /// + public void Perform() + { + string path = Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "encoding.xml"); + var oldSerializer = new XmlSerializer(typeof(OldEncodingOptions), new XmlRootAttribute("EncodingOptions")); + OldEncodingOptions? oldConfig = null; + + try + { + using var xmlReader = XmlReader.Create(path); + oldConfig = (OldEncodingOptions?)oldSerializer.Deserialize(xmlReader); + } + catch (InvalidOperationException ex) + { + _logger.LogError(ex, "Migrate EncodingOptions deserialize Invalid Operation error"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Migrate EncodingOptions deserialize error"); + } + + if (oldConfig is null) + { + return; + } + + var hardwareAccelerationType = HardwareAccelerationType.none; + if (Enum.TryParse(oldConfig.HardwareAccelerationType, true, out var parsedHardwareAccelerationType)) + { + hardwareAccelerationType = parsedHardwareAccelerationType; + } + + var tonemappingAlgorithm = TonemappingAlgorithm.none; + if (Enum.TryParse(oldConfig.TonemappingAlgorithm, true, out var parsedTonemappingAlgorithm)) + { + tonemappingAlgorithm = parsedTonemappingAlgorithm; + } + + var tonemappingMode = TonemappingMode.auto; + if (Enum.TryParse(oldConfig.TonemappingMode, true, out var parsedTonemappingMode)) + { + tonemappingMode = parsedTonemappingMode; + } + + var tonemappingRange = TonemappingRange.auto; + if (Enum.TryParse(oldConfig.TonemappingRange, true, out var parsedTonemappingRange)) + { + tonemappingRange = parsedTonemappingRange; + } + + var encoderPreset = EncoderPreset.superfast; + if (Enum.TryParse(oldConfig.TonemappingRange, true, out var parsedEncoderPreset)) + { + encoderPreset = parsedEncoderPreset; + } + + var deinterlaceMethod = DeinterlaceMethod.yadif; + if (Enum.TryParse(oldConfig.TonemappingRange, true, out var parsedDeinterlaceMethod)) + { + deinterlaceMethod = parsedDeinterlaceMethod; + } + + var encodingOptions = new EncodingOptions() + { + EncodingThreadCount = oldConfig.EncodingThreadCount, + TranscodingTempPath = oldConfig.TranscodingTempPath, + FallbackFontPath = oldConfig.FallbackFontPath, + EnableFallbackFont = oldConfig.EnableFallbackFont, + EnableAudioVbr = oldConfig.EnableAudioVbr, + DownMixAudioBoost = oldConfig.DownMixAudioBoost, + DownMixStereoAlgorithm = oldConfig.DownMixStereoAlgorithm, + MaxMuxingQueueSize = oldConfig.MaxMuxingQueueSize, + EnableThrottling = oldConfig.EnableThrottling, + ThrottleDelaySeconds = oldConfig.ThrottleDelaySeconds, + EnableSegmentDeletion = oldConfig.EnableSegmentDeletion, + SegmentKeepSeconds = oldConfig.SegmentKeepSeconds, + HardwareAccelerationType = hardwareAccelerationType, + EncoderAppPath = oldConfig.EncoderAppPath, + EncoderAppPathDisplay = oldConfig.EncoderAppPathDisplay, + VaapiDevice = oldConfig.VaapiDevice, + EnableTonemapping = oldConfig.EnableTonemapping, + EnableVppTonemapping = oldConfig.EnableVppTonemapping, + EnableVideoToolboxTonemapping = oldConfig.EnableVideoToolboxTonemapping, + TonemappingAlgorithm = tonemappingAlgorithm, + TonemappingMode = tonemappingMode, + TonemappingRange = tonemappingRange, + TonemappingDesat = oldConfig.TonemappingDesat, + TonemappingPeak = oldConfig.TonemappingPeak, + TonemappingParam = oldConfig.TonemappingParam, + VppTonemappingBrightness = oldConfig.VppTonemappingBrightness, + VppTonemappingContrast = oldConfig.VppTonemappingContrast, + H264Crf = oldConfig.H264Crf, + H265Crf = oldConfig.H265Crf, + EncoderPreset = encoderPreset, + DeinterlaceDoubleRate = oldConfig.DeinterlaceDoubleRate, + DeinterlaceMethod = deinterlaceMethod, + EnableDecodingColorDepth10Hevc = oldConfig.EnableDecodingColorDepth10Hevc, + EnableDecodingColorDepth10Vp9 = oldConfig.EnableDecodingColorDepth10Vp9, + EnableEnhancedNvdecDecoder = oldConfig.EnableEnhancedNvdecDecoder, + PreferSystemNativeHwDecoder = oldConfig.PreferSystemNativeHwDecoder, + EnableIntelLowPowerH264HwEncoder = oldConfig.EnableIntelLowPowerH264HwEncoder, + EnableIntelLowPowerHevcHwEncoder = oldConfig.EnableIntelLowPowerHevcHwEncoder, + EnableHardwareEncoding = oldConfig.EnableHardwareEncoding, + AllowHevcEncoding = oldConfig.AllowHevcEncoding, + AllowAv1Encoding = oldConfig.AllowAv1Encoding, + EnableSubtitleExtraction = oldConfig.EnableSubtitleExtraction, + HardwareDecodingCodecs = oldConfig.HardwareDecodingCodecs, + AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = oldConfig.AllowOnDemandMetadataBasedKeyframeExtractionForExtensions + }; + + var newSerializer = new XmlSerializer(typeof(EncodingOptions)); + var xmlWriterSettings = new XmlWriterSettings { Indent = true }; + using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings); + newSerializer.Serialize(xmlWriter, encodingOptions); + } + +#pragma warning disable + public sealed class OldEncodingOptions + { + public int EncodingThreadCount { get; set; } + + public string TranscodingTempPath { get; set; } + + public string FallbackFontPath { get; set; } + + public bool EnableFallbackFont { get; set; } + + public bool EnableAudioVbr { get; set; } + + public double DownMixAudioBoost { get; set; } + + public DownMixStereoAlgorithms DownMixStereoAlgorithm { get; set; } + + public int MaxMuxingQueueSize { get; set; } + + public bool EnableThrottling { get; set; } + + public int ThrottleDelaySeconds { get; set; } + + public bool EnableSegmentDeletion { get; set; } + + public int SegmentKeepSeconds { get; set; } + + public string HardwareAccelerationType { get; set; } + + public string EncoderAppPath { get; set; } + + public string EncoderAppPathDisplay { get; set; } + + public string VaapiDevice { get; set; } + + public bool EnableTonemapping { get; set; } + + public bool EnableVppTonemapping { get; set; } + + public bool EnableVideoToolboxTonemapping { get; set; } + + public string TonemappingAlgorithm { get; set; } + + public string TonemappingMode { get; set; } + + public string TonemappingRange { get; set; } + + public double TonemappingDesat { get; set; } + + public double TonemappingPeak { get; set; } + + public double TonemappingParam { get; set; } + + public double VppTonemappingBrightness { get; set; } + + public double VppTonemappingContrast { get; set; } + + public int H264Crf { get; set; } + + public int H265Crf { get; set; } + + public string EncoderPreset { get; set; } + + public bool DeinterlaceDoubleRate { get; set; } + + public string DeinterlaceMethod { get; set; } + + public bool EnableDecodingColorDepth10Hevc { get; set; } + + public bool EnableDecodingColorDepth10Vp9 { get; set; } + + public bool EnableEnhancedNvdecDecoder { get; set; } + + public bool PreferSystemNativeHwDecoder { get; set; } + + public bool EnableIntelLowPowerH264HwEncoder { get; set; } + + public bool EnableIntelLowPowerHevcHwEncoder { get; set; } + + public bool EnableHardwareEncoding { get; set; } + + public bool AllowHevcEncoding { get; set; } + + public bool AllowAv1Encoding { get; set; } + + public bool EnableSubtitleExtraction { get; set; } + + public string[] HardwareDecodingCodecs { get; set; } + + public string[] AllowOnDemandMetadataBasedKeyframeExtractionForExtensions { get; set; } + } +} diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs index 0544fe561a..580282a5f5 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs @@ -48,9 +48,11 @@ public class MigrateMusicBrainzTimeout : IMigrationRoutine if (oldPluginConfiguration is not null) { - var newPluginConfiguration = new PluginConfiguration(); - newPluginConfiguration.Server = oldPluginConfiguration.Server; - newPluginConfiguration.ReplaceArtistName = oldPluginConfiguration.ReplaceArtistName; + var newPluginConfiguration = new PluginConfiguration + { + Server = oldPluginConfiguration.Server, + ReplaceArtistName = oldPluginConfiguration.ReplaceArtistName + }; var newRateLimit = oldPluginConfiguration.RateLimit / 1000.0; newPluginConfiguration.RateLimit = newRateLimit < 1.0 ? 1.0 : newRateLimit; WriteNew(path, newPluginConfiguration); @@ -93,6 +95,4 @@ public class MigrateMusicBrainzTimeout : IMigrationRoutine public bool ReplaceArtistName { get; set; } } -#pragma warning restore - } diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs index d92c00991b..49960f4305 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs @@ -55,49 +55,53 @@ public class MigrateNetworkConfiguration : IMigrationRoutine _logger.LogError(ex, "Migrate NetworkConfiguration deserialize error"); } - if (oldNetworkConfiguration is not null) + if (oldNetworkConfiguration is null) { - // Migrate network config values to new config schema - var networkConfiguration = new NetworkConfiguration(); - networkConfiguration.AutoDiscovery = oldNetworkConfiguration.AutoDiscovery; - networkConfiguration.BaseUrl = oldNetworkConfiguration.BaseUrl; - networkConfiguration.CertificatePassword = oldNetworkConfiguration.CertificatePassword; - networkConfiguration.CertificatePath = oldNetworkConfiguration.CertificatePath; - networkConfiguration.EnableHttps = oldNetworkConfiguration.EnableHttps; - networkConfiguration.EnableIPv4 = oldNetworkConfiguration.EnableIPV4; - networkConfiguration.EnableIPv6 = oldNetworkConfiguration.EnableIPV6; - networkConfiguration.EnablePublishedServerUriByRequest = oldNetworkConfiguration.EnablePublishedServerUriByRequest; - networkConfiguration.EnableRemoteAccess = oldNetworkConfiguration.EnableRemoteAccess; - networkConfiguration.EnableUPnP = oldNetworkConfiguration.EnableUPnP; - networkConfiguration.IgnoreVirtualInterfaces = oldNetworkConfiguration.IgnoreVirtualInterfaces; - networkConfiguration.InternalHttpPort = oldNetworkConfiguration.HttpServerPortNumber; - networkConfiguration.InternalHttpsPort = oldNetworkConfiguration.HttpsPortNumber; - networkConfiguration.IsRemoteIPFilterBlacklist = oldNetworkConfiguration.IsRemoteIPFilterBlacklist; - networkConfiguration.KnownProxies = oldNetworkConfiguration.KnownProxies; - networkConfiguration.LocalNetworkAddresses = oldNetworkConfiguration.LocalNetworkAddresses; - networkConfiguration.LocalNetworkSubnets = oldNetworkConfiguration.LocalNetworkSubnets; - networkConfiguration.PublicHttpPort = oldNetworkConfiguration.PublicPort; - networkConfiguration.PublicHttpsPort = oldNetworkConfiguration.PublicHttpsPort; - networkConfiguration.PublishedServerUriBySubnet = oldNetworkConfiguration.PublishedServerUriBySubnet; - networkConfiguration.RemoteIPFilter = oldNetworkConfiguration.RemoteIPFilter; - networkConfiguration.RequireHttps = oldNetworkConfiguration.RequireHttps; - - // Migrate old virtual interface name schema - var oldVirtualInterfaceNames = oldNetworkConfiguration.VirtualInterfaceNames; - if (oldVirtualInterfaceNames.Equals("vEthernet*", StringComparison.OrdinalIgnoreCase)) - { - networkConfiguration.VirtualInterfaceNames = new string[] { "veth" }; - } - else - { - networkConfiguration.VirtualInterfaceNames = oldVirtualInterfaceNames.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase).Split(','); - } - - var networkConfigSerializer = new XmlSerializer(typeof(NetworkConfiguration)); - var xmlWriterSettings = new XmlWriterSettings { Indent = true }; - using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings); - networkConfigSerializer.Serialize(xmlWriter, networkConfiguration); + return; } + + // Migrate network config values to new config schema + var networkConfiguration = new NetworkConfiguration + { + AutoDiscovery = oldNetworkConfiguration.AutoDiscovery, + BaseUrl = oldNetworkConfiguration.BaseUrl, + CertificatePassword = oldNetworkConfiguration.CertificatePassword, + CertificatePath = oldNetworkConfiguration.CertificatePath, + EnableHttps = oldNetworkConfiguration.EnableHttps, + EnableIPv4 = oldNetworkConfiguration.EnableIPV4, + EnableIPv6 = oldNetworkConfiguration.EnableIPV6, + EnablePublishedServerUriByRequest = oldNetworkConfiguration.EnablePublishedServerUriByRequest, + EnableRemoteAccess = oldNetworkConfiguration.EnableRemoteAccess, + EnableUPnP = oldNetworkConfiguration.EnableUPnP, + IgnoreVirtualInterfaces = oldNetworkConfiguration.IgnoreVirtualInterfaces, + InternalHttpPort = oldNetworkConfiguration.HttpServerPortNumber, + InternalHttpsPort = oldNetworkConfiguration.HttpsPortNumber, + IsRemoteIPFilterBlacklist = oldNetworkConfiguration.IsRemoteIPFilterBlacklist, + KnownProxies = oldNetworkConfiguration.KnownProxies, + LocalNetworkAddresses = oldNetworkConfiguration.LocalNetworkAddresses, + LocalNetworkSubnets = oldNetworkConfiguration.LocalNetworkSubnets, + PublicHttpPort = oldNetworkConfiguration.PublicPort, + PublicHttpsPort = oldNetworkConfiguration.PublicHttpsPort, + PublishedServerUriBySubnet = oldNetworkConfiguration.PublishedServerUriBySubnet, + RemoteIPFilter = oldNetworkConfiguration.RemoteIPFilter, + RequireHttps = oldNetworkConfiguration.RequireHttps + }; + + // Migrate old virtual interface name schema + var oldVirtualInterfaceNames = oldNetworkConfiguration.VirtualInterfaceNames; + if (oldVirtualInterfaceNames.Equals("vEthernet*", StringComparison.OrdinalIgnoreCase)) + { + networkConfiguration.VirtualInterfaceNames = new string[] { "veth" }; + } + else + { + networkConfiguration.VirtualInterfaceNames = oldVirtualInterfaceNames.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase).Split(','); + } + + var networkConfigSerializer = new XmlSerializer(typeof(NetworkConfiguration)); + var xmlWriterSettings = new XmlWriterSettings { Indent = true }; + using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings); + networkConfigSerializer.Serialize(xmlWriter, networkConfiguration); } #pragma warning disable @@ -204,5 +208,4 @@ public class MigrateNetworkConfiguration : IMigrationRoutine public bool EnablePublishedServerUriByRequest { get; set; } = false; } -#pragma warning restore } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index d6ad7e2b35..fdc56652ae 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -37,6 +37,8 @@ namespace MediaBrowser.Controller.MediaEncoding /// public const string ValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$"; + private const string _defaultMjpegEncoder = "mjpeg"; + private const string QsvAlias = "qs"; private const string VaapiAlias = "va"; private const string D3d11vaAlias = "dx11"; @@ -72,8 +74,8 @@ namespace MediaBrowser.Controller.MediaEncoding private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled); - private static readonly string[] _videoProfilesH264 = new[] - { + private static readonly string[] _videoProfilesH264 = + [ "ConstrainedBaseline", "Baseline", "Extended", @@ -82,20 +84,20 @@ namespace MediaBrowser.Controller.MediaEncoding "ProgressiveHigh", "ConstrainedHigh", "High10" - }; + ]; - private static readonly string[] _videoProfilesH265 = new[] - { + private static readonly string[] _videoProfilesH265 = + [ "Main", "Main10" - }; + ]; - private static readonly string[] _videoProfilesAv1 = new[] - { + private static readonly string[] _videoProfilesAv1 = + [ "Main", "High", "Professional", - }; + ]; private static readonly HashSet _mp4ContainerNames = new(StringComparer.OrdinalIgnoreCase) { @@ -107,8 +109,8 @@ namespace MediaBrowser.Controller.MediaEncoding "m4v", }; - private static readonly string[] _legacyTonemapModes = new[] { "max", "rgb" }; - private static readonly string[] _advancedTonemapModes = new[] { "lum", "itp" }; + private static readonly TonemappingMode[] _legacyTonemapModes = [TonemappingMode.max, TonemappingMode.rgb]; + private static readonly TonemappingMode[] _advancedTonemapModes = [TonemappingMode.lum, TonemappingMode.itp]; // Set max transcoding channels for encoders that can't handle more than a set amount of channels // AAC, FLAC, ALAC, libopus, libvorbis encoders all support at least 8 channels @@ -123,23 +125,22 @@ namespace MediaBrowser.Controller.MediaEncoding { "truehd", 6 }, }; - private static readonly string _defaultMjpegEncoder = "mjpeg"; - private static readonly Dictionary _mjpegCodecMap = new(StringComparer.OrdinalIgnoreCase) + private static readonly Dictionary _mjpegCodecMap = new() { - { "vaapi", _defaultMjpegEncoder + "_vaapi" }, - { "qsv", _defaultMjpegEncoder + "_qsv" }, - { "videotoolbox", _defaultMjpegEncoder + "_videotoolbox" } + { HardwareAccelerationType.vaapi, _defaultMjpegEncoder + "_vaapi" }, + { HardwareAccelerationType.qsv, _defaultMjpegEncoder + "_qsv" }, + { HardwareAccelerationType.videotoolbox, _defaultMjpegEncoder + "_videotoolbox" } }; - public static readonly string[] LosslessAudioCodecs = new string[] - { + public static readonly string[] LosslessAudioCodecs = + [ "alac", "ape", "flac", "mlp", "truehd", "wavpack" - }; + ]; public EncodingHelper( IApplicationPaths appPaths, @@ -176,18 +177,18 @@ namespace MediaBrowser.Controller.MediaEncoding { var hwType = encodingOptions.HardwareAccelerationType; - var codecMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + var codecMap = new Dictionary() { - { "amf", hwEncoder + "_amf" }, - { "nvenc", hwEncoder + "_nvenc" }, - { "qsv", hwEncoder + "_qsv" }, - { "vaapi", hwEncoder + "_vaapi" }, - { "videotoolbox", hwEncoder + "_videotoolbox" }, - { "v4l2m2m", hwEncoder + "_v4l2m2m" }, - { "rkmpp", hwEncoder + "_rkmpp" }, + { HardwareAccelerationType.amf, hwEncoder + "_amf" }, + { HardwareAccelerationType.nvenc, hwEncoder + "_nvenc" }, + { HardwareAccelerationType.qsv, hwEncoder + "_qsv" }, + { HardwareAccelerationType.vaapi, hwEncoder + "_vaapi" }, + { HardwareAccelerationType.videotoolbox, hwEncoder + "_videotoolbox" }, + { HardwareAccelerationType.v4l2m2m, hwEncoder + "_v4l2m2m" }, + { HardwareAccelerationType.rkmpp, hwEncoder + "_rkmpp" }, }; - if (!string.IsNullOrEmpty(hwType) + if (hwType != HardwareAccelerationType.none && encodingOptions.EnableHardwareEncoding && codecMap.TryGetValue(hwType, out var preferredEncoder) && _mediaEncoder.SupportsEncoder(preferredEncoder)) @@ -205,7 +206,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var hwType = encodingOptions.HardwareAccelerationType; - if (!string.IsNullOrEmpty(hwType) + if (hwType != HardwareAccelerationType.none && encodingOptions.EnableHardwareEncoding && _mjpegCodecMap.TryGetValue(hwType, out var preferredEncoder) && _mediaEncoder.SupportsEncoder(preferredEncoder)) @@ -360,7 +361,7 @@ namespace MediaBrowser.Controller.MediaEncoding // prefer 'tonemap_vaapi' over 'vpp_qsv' on Linux for supporting Gen9/KBLx. // 'vpp_qsv' requires VPL, which is only supported on Gen12/TGLx and newer. if (OperatingSystem.IsWindows() - && string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) + && options.HardwareAccelerationType == HardwareAccelerationType.qsv && _mediaEncoder.EncoderVersion < _minFFmpegQsvVppTonemapOption) { return false; @@ -970,7 +971,7 @@ namespace MediaBrowser.Controller.MediaEncoding var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty; var isHwTonemapAvailable = IsHwTonemapAvailable(state, options); - if (string.Equals(optHwaccelType, "vaapi", StringComparison.OrdinalIgnoreCase)) + if (optHwaccelType == HardwareAccelerationType.vaapi) { if (!isLinux || !_mediaEncoder.SupportsHwaccel("vaapi")) { @@ -1044,7 +1045,7 @@ namespace MediaBrowser.Controller.MediaEncoding args.Append(filterDevArgs); } - else if (string.Equals(optHwaccelType, "qsv", StringComparison.OrdinalIgnoreCase)) + else if (optHwaccelType == HardwareAccelerationType.qsv) { if ((!isLinux && !isWindows) || !_mediaEncoder.SupportsHwaccel("qsv")) { @@ -1079,7 +1080,7 @@ namespace MediaBrowser.Controller.MediaEncoding args.Append(filterDevArgs); } - else if (string.Equals(optHwaccelType, "nvenc", StringComparison.OrdinalIgnoreCase)) + else if (optHwaccelType == HardwareAccelerationType.nvenc) { if ((!isLinux && !isWindows) || !IsCudaFullSupported()) { @@ -1098,7 +1099,7 @@ namespace MediaBrowser.Controller.MediaEncoding args.Append(GetCudaDeviceArgs(0, CudaAlias)) .Append(GetFilterHwDeviceArgs(CudaAlias)); } - else if (string.Equals(optHwaccelType, "amf", StringComparison.OrdinalIgnoreCase)) + else if (optHwaccelType == HardwareAccelerationType.amf) { if (!isWindows || !_mediaEncoder.SupportsHwaccel("d3d11va")) { @@ -1123,7 +1124,7 @@ namespace MediaBrowser.Controller.MediaEncoding args.Append(filterDevArgs); } - else if (string.Equals(optHwaccelType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + else if (optHwaccelType == HardwareAccelerationType.videotoolbox) { if (!isMacOS || !_mediaEncoder.SupportsHwaccel("videotoolbox")) { @@ -1140,7 +1141,7 @@ namespace MediaBrowser.Controller.MediaEncoding // videotoolbox hw filter does not require device selection args.Append(GetVideoToolboxDeviceArgs(VideotoolboxAlias)); } - else if (string.Equals(optHwaccelType, "rkmpp", StringComparison.OrdinalIgnoreCase)) + else if (optHwaccelType == HardwareAccelerationType.rkmpp) { if (!isLinux || !_mediaEncoder.SupportsHwaccel("rkmpp")) { @@ -1413,6 +1414,149 @@ namespace MediaBrowser.Controller.MediaEncoding return FormattableString.Invariant($" -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}"); } + private string GetEncoderParam(EncoderPreset? preset, EncoderPreset defaultPreset, EncodingOptions encodingOptions, string videoEncoder, bool isLibX265) + { + var param = string.Empty; + var encoderPreset = preset ?? defaultPreset; + if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) || isLibX265) + { + param += " -preset " + encoderPreset.ToString().ToLowerInvariant(); + + int encodeCrf = encodingOptions.H264Crf; + if (isLibX265) + { + encodeCrf = encodingOptions.H265Crf; + } + + if (encodeCrf >= 0 && encodeCrf <= 51) + { + param += " -crf " + encodeCrf.ToString(CultureInfo.InvariantCulture); + } + else + { + string defaultCrf = "23"; + if (isLibX265) + { + defaultCrf = "28"; + } + + param += " -crf " + defaultCrf; + } + } + else if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)) + { + // Default to use the recommended preset 10. + // Omit presets < 5, which are too slow for on the fly encoding. + // https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/Ffmpeg.md + param += encoderPreset switch + { + EncoderPreset.veryslow => " -preset 5", + EncoderPreset.slower => " -preset 6", + EncoderPreset.slow => " -preset 7", + EncoderPreset.medium => " -preset 8", + EncoderPreset.fast => " -preset 9", + EncoderPreset.faster => " -preset 10", + EncoderPreset.veryfast => " -preset 11", + EncoderPreset.superfast => " -preset 12", + EncoderPreset.ultrafast => " -preset 13", + _ => " -preset 10" + }; + } + else if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "av1_vaapi", StringComparison.OrdinalIgnoreCase)) + { + // -compression_level is not reliable on AMD. + if (_mediaEncoder.IsVaapiDeviceInteliHD) + { + param += encoderPreset switch + { + EncoderPreset.veryslow => " -compression_level 1", + EncoderPreset.slower => " -compression_level 2", + EncoderPreset.slow => " -compression_level 3", + EncoderPreset.medium => " -compression_level 4", + EncoderPreset.fast => " -compression_level 5", + EncoderPreset.faster => " -compression_level 6", + EncoderPreset.veryfast => " -compression_level 7", + EncoderPreset.superfast => " -compression_level 7", + EncoderPreset.ultrafast => " -compression_level 7", + _ => string.Empty + }; + } + } + else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv) + || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase) // hevc (hevc_qsv) + || string.Equals(videoEncoder, "av1_qsv", StringComparison.OrdinalIgnoreCase)) // av1 (av1_qsv) + { + EncoderPreset[] valid_presets = [EncoderPreset.veryslow, EncoderPreset.slower, EncoderPreset.slow, EncoderPreset.medium, EncoderPreset.fast, EncoderPreset.faster, EncoderPreset.veryfast]; + + if (valid_presets.Contains(encoderPreset)) + { + param += " -preset " + encodingOptions.EncoderPreset; + } + else + { + param += " -preset " + EncoderPreset.veryfast.ToString().ToLowerInvariant(); + } + } + else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc) + || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) // hevc (hevc_nvenc) + || string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase) // av1 (av1_nvenc) + ) + { + param += encoderPreset switch + { + EncoderPreset.veryslow => " -preset p7", + EncoderPreset.slower => " -preset p6", + EncoderPreset.slow => " -preset p5", + EncoderPreset.medium => " -preset p4", + EncoderPreset.fast => " -preset p3", + EncoderPreset.faster => " -preset p2", + _ => " -preset p1" + }; + } + else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf) + || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) // hevc (hevc_amf) + || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase) // av1 (av1_amf) + ) + { + param += encoderPreset switch + { + EncoderPreset.veryslow => " -quality quality", + EncoderPreset.slower => " -quality quality", + EncoderPreset.slow => " -quality quality", + EncoderPreset.medium => " -quality balanced", + _ => " -quality speed" + }; + + if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase)) + { + param += " -header_insertion_mode gop"; + } + + if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) + { + param += " -gops_per_idr 1"; + } + } + else if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase) // h264 (h264_videotoolbox) + || string.Equals(videoEncoder, "hevc_videotoolbox", StringComparison.OrdinalIgnoreCase) // hevc (hevc_videotoolbox) + ) + { + param += encoderPreset switch + { + EncoderPreset.veryslow => " -prio_speed 0", + EncoderPreset.slower => " -prio_speed 0", + EncoderPreset.slow => " -prio_speed 0", + EncoderPreset.medium => " -prio_speed 0", + _ => " -prio_speed 1" + }; + } + + return param; + } + public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level) { if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel)) @@ -1625,7 +1769,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// Encoding options. /// Default present to use for encoding. /// Video bitrate. - public string GetVideoQualityParam(EncodingJobInfo state, string videoEncoder, EncodingOptions encodingOptions, string defaultPreset) + public string GetVideoQualityParam(EncodingJobInfo state, string videoEncoder, EncodingOptions encodingOptions, EncoderPreset defaultPreset) { var param = string.Empty; @@ -1640,7 +1784,9 @@ namespace MediaBrowser.Controller.MediaEncoding // https://github.com/intel/media-driver/issues/1456 var enableWaFori915Hang = false; - if (string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) + var hardwareAccelerationType = encodingOptions.HardwareAccelerationType; + + if (hardwareAccelerationType == HardwareAccelerationType.vaapi) { var isIntelVaapiDriver = _mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965; @@ -1653,7 +1799,7 @@ namespace MediaBrowser.Controller.MediaEncoding intelLowPowerHwEncoding = encodingOptions.EnableIntelLowPowerHevcHwEncoder && isIntelVaapiDriver; } } - else if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) + else if (hardwareAccelerationType == HardwareAccelerationType.qsv) { if (OperatingSystem.IsLinux()) { @@ -1700,204 +1846,10 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -async_depth 1"; } - var isVc1 = string.Equals(state.VideoStream?.Codec, "vc1", StringComparison.OrdinalIgnoreCase); var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase); + var encodingPreset = encodingOptions.EncoderPreset; - if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) || isLibX265) - { - if (!string.IsNullOrEmpty(encodingOptions.EncoderPreset)) - { - param += " -preset " + encodingOptions.EncoderPreset; - } - else - { - param += " -preset " + defaultPreset; - } - - int encodeCrf = encodingOptions.H264Crf; - if (isLibX265) - { - encodeCrf = encodingOptions.H265Crf; - } - - if (encodeCrf >= 0 && encodeCrf <= 51) - { - param += " -crf " + encodeCrf.ToString(CultureInfo.InvariantCulture); - } - else - { - string defaultCrf = "23"; - if (isLibX265) - { - defaultCrf = "28"; - } - - param += " -crf " + defaultCrf; - } - } - else if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)) - { - // Default to use the recommended preset 10. - // Omit presets < 5, which are too slow for on the fly encoding. - // https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/Ffmpeg.md - param += encodingOptions.EncoderPreset switch - { - "veryslow" => " -preset 5", - "slower" => " -preset 6", - "slow" => " -preset 7", - "medium" => " -preset 8", - "fast" => " -preset 9", - "faster" => " -preset 10", - "veryfast" => " -preset 11", - "superfast" => " -preset 12", - "ultrafast" => " -preset 13", - _ => " -preset 10" - }; - } - else if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoEncoder, "av1_vaapi", StringComparison.OrdinalIgnoreCase)) - { - // -compression_level is not reliable on AMD. - if (_mediaEncoder.IsVaapiDeviceInteliHD) - { - param += encodingOptions.EncoderPreset switch - { - "veryslow" => " -compression_level 1", - "slower" => " -compression_level 2", - "slow" => " -compression_level 3", - "medium" => " -compression_level 4", - "fast" => " -compression_level 5", - "faster" => " -compression_level 6", - "veryfast" => " -compression_level 7", - "superfast" => " -compression_level 7", - "ultrafast" => " -compression_level 7", - _ => string.Empty - }; - } - } - else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv) - || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase) // hevc (hevc_qsv) - || string.Equals(videoEncoder, "av1_qsv", StringComparison.OrdinalIgnoreCase)) // av1 (av1_qsv) - { - string[] valid_presets = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" }; - - if (valid_presets.Contains(encodingOptions.EncoderPreset, StringComparison.OrdinalIgnoreCase)) - { - param += " -preset " + encodingOptions.EncoderPreset; - } - else - { - param += " -preset veryfast"; - } - } - else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc) - || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) // hevc (hevc_nvenc) - || string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase)) // av1 (av1_nvenc) - { - switch (encodingOptions.EncoderPreset) - { - case "veryslow": - param += " -preset p7"; - break; - - case "slower": - param += " -preset p6"; - break; - - case "slow": - param += " -preset p5"; - break; - - case "medium": - param += " -preset p4"; - break; - - case "fast": - param += " -preset p3"; - break; - - case "faster": - param += " -preset p2"; - break; - - case "veryfast": - case "superfast": - case "ultrafast": - param += " -preset p1"; - break; - - default: - param += " -preset p1"; - break; - } - } - else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf) - || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) // hevc (hevc_amf) - || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase)) // av1 (av1_amf) - { - switch (encodingOptions.EncoderPreset) - { - case "veryslow": - case "slower": - case "slow": - param += " -quality quality"; - break; - - case "medium": - param += " -quality balanced"; - break; - - case "fast": - case "faster": - case "veryfast": - case "superfast": - case "ultrafast": - param += " -quality speed"; - break; - - default: - param += " -quality speed"; - break; - } - - if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase)) - { - param += " -header_insertion_mode gop"; - } - - if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) - { - param += " -gops_per_idr 1"; - } - } - else if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase) // h264 (h264_videotoolbox) - || string.Equals(videoEncoder, "hevc_videotoolbox", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_videotoolbox) - { - switch (encodingOptions.EncoderPreset) - { - case "veryslow": - case "slower": - case "slow": - case "medium": - param += " -prio_speed 0"; - break; - - case "fast": - case "faster": - case "veryfast": - case "superfast": - case "ultrafast": - param += " -prio_speed 1"; - break; - - default: - param += " -prio_speed 1"; - break; - } - } - + param += GetEncoderParam(encodingPreset, defaultPreset, encodingOptions, videoEncoder, isLibX265); param += GetVideoBitrateParam(state, videoEncoder); var framerate = GetFramerateParam(state); @@ -3256,7 +3208,7 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Format( CultureInfo.InvariantCulture, "{0}={1}:-1:0", - string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase) ? "bwdif" : "yadif", + options.DeinterlaceMethod.ToString().ToLowerInvariant(), doubleRateDeint ? "1" : "0"); } @@ -3265,8 +3217,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.ReferenceFrameRate ?? 60) <= 30; if (hwDeintSuffix.Contains("cuda", StringComparison.OrdinalIgnoreCase)) { - var useBwdif = string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase) - && _mediaEncoder.SupportsFilter("bwdif_cuda"); + var useBwdif = options.DeinterlaceMethod == DeinterlaceMethod.bwdif && _mediaEncoder.SupportsFilter("bwdif_cuda"); return string.Format( CultureInfo.InvariantCulture, @@ -3307,7 +3258,10 @@ namespace MediaBrowser.Controller.MediaEncoding } var args = string.Empty; - var algorithm = options.TonemappingAlgorithm; + var algorithm = options.TonemappingAlgorithm.ToString().ToLowerInvariant(); + var mode = options.TonemappingMode.ToString().ToLowerInvariant(); + var range = options.TonemappingRange; + var rangeString = range.ToString().ToLowerInvariant(); if (string.Equals(hwTonemapSuffix, "vaapi", StringComparison.OrdinalIgnoreCase)) { @@ -3342,10 +3296,10 @@ namespace MediaBrowser.Controller.MediaEncoding args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709:tonemap={2}:peak={3}:desat={4}"; var useLegacyTonemapModes = _mediaEncoder.EncoderVersion >= _minFFmpegOclCuTonemapMode - && _legacyTonemapModes.Contains(options.TonemappingMode, StringComparison.OrdinalIgnoreCase); + && _legacyTonemapModes.Contains(options.TonemappingMode); var useAdvancedTonemapModes = _mediaEncoder.EncoderVersion >= _minFFmpegAdvancedTonemapMode - && _advancedTonemapModes.Contains(options.TonemappingMode, StringComparison.OrdinalIgnoreCase); + && _advancedTonemapModes.Contains(options.TonemappingMode); if (useLegacyTonemapModes || useAdvancedTonemapModes) { @@ -3357,8 +3311,7 @@ namespace MediaBrowser.Controller.MediaEncoding args += ":param={6}"; } - if (string.Equals(options.TonemappingRange, "tv", StringComparison.OrdinalIgnoreCase) - || string.Equals(options.TonemappingRange, "pc", StringComparison.OrdinalIgnoreCase)) + if (range == TonemappingRange.tv || range == TonemappingRange.pc) { args += ":range={7}"; } @@ -3372,9 +3325,9 @@ namespace MediaBrowser.Controller.MediaEncoding algorithm, options.TonemappingPeak, options.TonemappingDesat, - options.TonemappingMode, + mode, options.TonemappingParam, - options.TonemappingRange); + rangeString); } public string GetLibplaceboFilter( @@ -3409,24 +3362,24 @@ namespace MediaBrowser.Controller.MediaEncoding if (doTonemap) { var algorithm = options.TonemappingAlgorithm; + var algorithmString = "clip"; var mode = options.TonemappingMode; var range = options.TonemappingRange; - if (string.Equals(algorithm, "bt2390", StringComparison.OrdinalIgnoreCase)) + if (algorithm == TonemappingAlgorithm.bt2390) { - algorithm = "bt.2390"; + algorithmString = "bt.2390"; } - else if (string.Equals(algorithm, "none", StringComparison.OrdinalIgnoreCase)) + else if (algorithm != TonemappingAlgorithm.none) { - algorithm = "clip"; + algorithmString = algorithm.ToString().ToLowerInvariant(); } tonemapArg = ":tonemapping=" + algorithm + ":peak_detect=0:color_primaries=bt709:color_trc=bt709:colorspace=bt709"; - if (string.Equals(range, "tv", StringComparison.OrdinalIgnoreCase) - || string.Equals(range, "pc", StringComparison.OrdinalIgnoreCase)) + if (range == TonemappingRange.tv || range == TonemappingRange.pc) { - tonemapArg += ":range=" + range; + tonemapArg += ":range=" + range.ToString().ToLowerInvariant(); } } @@ -3530,8 +3483,8 @@ namespace MediaBrowser.Controller.MediaEncoding tonemapArgs += $":param={options.TonemappingParam}"; } - if (string.Equals(options.TonemappingRange, "tv", StringComparison.OrdinalIgnoreCase) - || string.Equals(options.TonemappingRange, "pc", StringComparison.OrdinalIgnoreCase)) + var range = options.TonemappingRange; + if (range == TonemappingRange.tv || range == TonemappingRange.pc) { tonemapArgs += $":range={options.TonemappingRange}"; } @@ -3575,7 +3528,7 @@ namespace MediaBrowser.Controller.MediaEncoding EncodingOptions options, string vidEncoder) { - if (!string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) + if (options.HardwareAccelerationType != HardwareAccelerationType.nvenc) { return (null, null, null); } @@ -3777,7 +3730,7 @@ namespace MediaBrowser.Controller.MediaEncoding EncodingOptions options, string vidEncoder) { - if (!string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)) + if (options.HardwareAccelerationType != HardwareAccelerationType.amf) { return (null, null, null); } @@ -3993,7 +3946,7 @@ namespace MediaBrowser.Controller.MediaEncoding EncodingOptions options, string vidEncoder) { - if (!string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) + if (options.HardwareAccelerationType != HardwareAccelerationType.qsv) { return (null, null, null); } @@ -4543,7 +4496,7 @@ namespace MediaBrowser.Controller.MediaEncoding EncodingOptions options, string vidEncoder) { - if (!string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) + if (options.HardwareAccelerationType != HardwareAccelerationType.vaapi) { return (null, null, null); } @@ -5247,7 +5200,7 @@ namespace MediaBrowser.Controller.MediaEncoding EncodingOptions options, string vidEncoder) { - if (!string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + if (options.HardwareAccelerationType != HardwareAccelerationType.videotoolbox) { return (null, null, null); } @@ -5436,7 +5389,7 @@ namespace MediaBrowser.Controller.MediaEncoding EncodingOptions options, string vidEncoder) { - if (!string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase)) + if (options.HardwareAccelerationType != HardwareAccelerationType.rkmpp) { return (null, null, null); } @@ -5696,38 +5649,20 @@ namespace MediaBrowser.Controller.MediaEncoding List subFilters; List overlayFilters; - if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) + (mainFilters, subFilters, overlayFilters) = options.HardwareAccelerationType switch { - (mainFilters, subFilters, overlayFilters) = GetVaapiVidFilterChain(state, options, outputVideoCodec); - } - else if (string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) - { - (mainFilters, subFilters, overlayFilters) = GetIntelVidFilterChain(state, options, outputVideoCodec); - } - else if (string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) - { - (mainFilters, subFilters, overlayFilters) = GetNvidiaVidFilterChain(state, options, outputVideoCodec); - } - else if (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)) - { - (mainFilters, subFilters, overlayFilters) = GetAmdVidFilterChain(state, options, outputVideoCodec); - } - else if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) - { - (mainFilters, subFilters, overlayFilters) = GetAppleVidFilterChain(state, options, outputVideoCodec); - } - else if (string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase)) - { - (mainFilters, subFilters, overlayFilters) = GetRkmppVidFilterChain(state, options, outputVideoCodec); - } - else - { - (mainFilters, subFilters, overlayFilters) = GetSwVidFilterChain(state, options, outputVideoCodec); - } + HardwareAccelerationType.vaapi => GetVaapiVidFilterChain(state, options, outputVideoCodec), + HardwareAccelerationType.amf => GetAmdVidFilterChain(state, options, outputVideoCodec), + HardwareAccelerationType.qsv => GetIntelVidFilterChain(state, options, outputVideoCodec), + HardwareAccelerationType.nvenc => GetNvidiaVidFilterChain(state, options, outputVideoCodec), + HardwareAccelerationType.videotoolbox => GetAppleVidFilterChain(state, options, outputVideoCodec), + HardwareAccelerationType.rkmpp => GetRkmppVidFilterChain(state, options, outputVideoCodec), + _ => GetSwVidFilterChain(state, options, outputVideoCodec), + }; - mainFilters?.RemoveAll(filter => string.IsNullOrEmpty(filter)); - subFilters?.RemoveAll(filter => string.IsNullOrEmpty(filter)); - overlayFilters?.RemoveAll(filter => string.IsNullOrEmpty(filter)); + mainFilters?.RemoveAll(string.IsNullOrEmpty); + subFilters?.RemoveAll(string.IsNullOrEmpty); + overlayFilters?.RemoveAll(string.IsNullOrEmpty); var framerate = GetFramerateParam(state); if (framerate.HasValue) @@ -5907,7 +5842,9 @@ namespace MediaBrowser.Controller.MediaEncoding return null; } - if (!string.IsNullOrEmpty(videoStream.Codec) && !string.IsNullOrEmpty(options.HardwareAccelerationType)) + var hardwareAccelerationType = options.HardwareAccelerationType; + + if (!string.IsNullOrEmpty(videoStream.Codec) && hardwareAccelerationType != HardwareAccelerationType.none) { var bitDepth = GetVideoColorBitDepth(state); @@ -5919,10 +5856,10 @@ namespace MediaBrowser.Controller.MediaEncoding || string.Equals(videoStream.Codec, "av1", StringComparison.OrdinalIgnoreCase))) { // RKMPP has H.264 Hi10P decoder - bool hasHardwareHi10P = string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase); + bool hasHardwareHi10P = hardwareAccelerationType == HardwareAccelerationType.rkmpp; // VideoToolbox on Apple Silicon has H.264 Hi10P mode enabled after macOS 14.6 - if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + if (hardwareAccelerationType == HardwareAccelerationType.videotoolbox) { var ver = Environment.OSVersion.Version; var arch = RuntimeInformation.OSArchitecture; @@ -5939,34 +5876,20 @@ namespace MediaBrowser.Controller.MediaEncoding } } - if (string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) + var decoder = hardwareAccelerationType switch { - return GetQsvHwVidDecoder(state, options, videoStream, bitDepth); - } + HardwareAccelerationType.vaapi => GetVaapiVidDecoder(state, options, videoStream, bitDepth), + HardwareAccelerationType.amf => GetAmfVidDecoder(state, options, videoStream, bitDepth), + HardwareAccelerationType.qsv => GetQsvHwVidDecoder(state, options, videoStream, bitDepth), + HardwareAccelerationType.nvenc => GetNvdecVidDecoder(state, options, videoStream, bitDepth), + HardwareAccelerationType.videotoolbox => GetVideotoolboxVidDecoder(state, options, videoStream, bitDepth), + HardwareAccelerationType.rkmpp => GetRkmppVidDecoder(state, options, videoStream, bitDepth), + _ => string.Empty + }; - if (string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(decoder)) { - return GetNvdecVidDecoder(state, options, videoStream, bitDepth); - } - - if (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)) - { - return GetAmfVidDecoder(state, options, videoStream, bitDepth); - } - - if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) - { - return GetVaapiVidDecoder(state, options, videoStream, bitDepth); - } - - if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) - { - return GetVideotoolboxVidDecoder(state, options, videoStream, bitDepth); - } - - if (string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase)) - { - return GetRkmppVidDecoder(state, options, videoStream, bitDepth); + return decoder; } } @@ -5981,7 +5904,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // Avoid a second attempt if no hardware acceleration is being used - options.HardwareDecodingCodecs = Array.FindAll(options.HardwareDecodingCodecs, val => !string.Equals(val, whichCodec, StringComparison.OrdinalIgnoreCase)); + options.HardwareDecodingCodecs = options.HardwareDecodingCodecs.Where(c => !string.Equals(c, whichCodec, StringComparison.OrdinalIgnoreCase)).ToArray(); // leave blank so ffmpeg will decide return null; @@ -6062,6 +5985,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isVideotoolboxSupported = isMacOS && _mediaEncoder.SupportsHwaccel("videotoolbox"); var isRkmppSupported = isLinux && IsRkmppFullSupported(); var isCodecAvailable = options.HardwareDecodingCodecs.Contains(videoCodec, StringComparison.OrdinalIgnoreCase); + var hardwareAccelerationType = options.HardwareAccelerationType; var ffmpegVersion = _mediaEncoder.EncoderVersion; @@ -6099,7 +6023,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // Intel qsv/d3d11va/vaapi - if (string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) + if (hardwareAccelerationType == HardwareAccelerationType.qsv) { if (options.PreferSystemNativeHwDecoder) { @@ -6125,7 +6049,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // Nvidia cuda - if (string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) + if (hardwareAccelerationType == HardwareAccelerationType.nvenc) { if (isCudaSupported && isCodecAvailable) { @@ -6142,7 +6066,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // Amd d3d11va - if (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)) + if (hardwareAccelerationType == HardwareAccelerationType.amf) { if (isD3d11Supported && isCodecAvailable) { @@ -6152,7 +6076,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // Vaapi - if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) + if (hardwareAccelerationType == HardwareAccelerationType.vaapi && isVaapiSupported && isCodecAvailable) { @@ -6161,7 +6085,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // Apple videotoolbox - if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase) + if (hardwareAccelerationType == HardwareAccelerationType.videotoolbox && isVideotoolboxSupported && isCodecAvailable) { @@ -6169,7 +6093,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // Rockchip rkmpp - if (string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase) + if (hardwareAccelerationType == HardwareAccelerationType.rkmpp && isRkmppSupported && isCodecAvailable) { @@ -6185,7 +6109,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isLinux = OperatingSystem.IsLinux(); if ((!isWindows && !isLinux) - || !string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) + || options.HardwareAccelerationType != HardwareAccelerationType.qsv) { return null; } @@ -6254,7 +6178,7 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetNvdecVidDecoder(EncodingJobInfo state, EncodingOptions options, MediaStream videoStream, int bitDepth) { if ((!OperatingSystem.IsWindows() && !OperatingSystem.IsLinux()) - || !string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) + || options.HardwareAccelerationType != HardwareAccelerationType.nvenc) { return null; } @@ -6319,7 +6243,7 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetAmfVidDecoder(EncodingJobInfo state, EncodingOptions options, MediaStream videoStream, int bitDepth) { if (!OperatingSystem.IsWindows() - || !string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)) + || options.HardwareAccelerationType != HardwareAccelerationType.amf) { return null; } @@ -6375,7 +6299,7 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetVaapiVidDecoder(EncodingJobInfo state, EncodingOptions options, MediaStream videoStream, int bitDepth) { if (!OperatingSystem.IsLinux() - || !string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) + || options.HardwareAccelerationType != HardwareAccelerationType.vaapi) { return null; } @@ -6437,7 +6361,7 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetVideotoolboxVidDecoder(EncodingJobInfo state, EncodingOptions options, MediaStream videoStream, int bitDepth) { if (!OperatingSystem.IsMacOS() - || !string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + || options.HardwareAccelerationType != HardwareAccelerationType.videotoolbox) { return null; } @@ -6485,7 +6409,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isLinux = OperatingSystem.IsLinux(); if (!isLinux - || !string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase)) + || options.HardwareAccelerationType != HardwareAccelerationType.rkmpp) { return null; } @@ -6749,7 +6673,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.IsVideoRequest) { - if (!string.IsNullOrEmpty(state.InputContainer) && state.VideoType == VideoType.VideoFile && string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType)) + if (!string.IsNullOrEmpty(state.InputContainer) && state.VideoType == VideoType.VideoFile && encodingOptions.HardwareAccelerationType != HardwareAccelerationType.none) { var inputFormat = GetInputFormat(state.InputContainer); if (!string.IsNullOrEmpty(inputFormat)) @@ -6865,7 +6789,7 @@ namespace MediaBrowser.Controller.MediaEncoding state.SupportedAudioCodecs = supportedAudioCodecsList.ToArray(); - request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => _mediaEncoder.CanEncodeToAudioCodec(i)) + request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(_mediaEncoder.CanEncodeToAudioCodec) ?? state.SupportedAudioCodecs.FirstOrDefault(); } @@ -6991,7 +6915,7 @@ namespace MediaBrowser.Controller.MediaEncoding return " -codec:s:0 " + codec + " -disposition:s:0 default"; } - public string GetProgressiveVideoFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, string defaultPreset) + public string GetProgressiveVideoFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, EncoderPreset defaultPreset) { // Get the output codec name var videoCodec = GetVideoEncoder(state, encodingOptions); @@ -7042,7 +6966,7 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Empty; } - public string GetProgressiveVideoArguments(EncodingJobInfo state, EncodingOptions encodingOptions, string videoCodec, string defaultPreset) + public string GetProgressiveVideoArguments(EncodingJobInfo state, EncodingOptions encodingOptions, string videoCodec, EncoderPreset defaultPreset) { var args = "-codec:v:0 " + videoCodec; diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index a6a443f3dd..caa9cb499d 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -224,7 +224,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (OperatingSystem.IsLinux() && SupportsHwaccel("vaapi") && !string.IsNullOrEmpty(options.VaapiDevice) - && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) + && options.HardwareAccelerationType == HardwareAccelerationType.vaapi) { _isVaapiDeviceAmd = validator.CheckVaapiDeviceByDriverName("Mesa Gallium driver", options.VaapiDevice); _isVaapiDeviceInteliHD = validator.CheckVaapiDeviceByDriverName("Intel iHD driver", options.VaapiDevice); @@ -799,11 +799,12 @@ namespace MediaBrowser.MediaEncoding.Encoder if (allowHwAccel && enableKeyFrameOnlyExtraction) { - var supportsKeyFrameOnly = (string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && options.EnableEnhancedNvdecDecoder) - || (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && OperatingSystem.IsWindows()) - || (string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && options.PreferSystemNativeHwDecoder) - || string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) - || string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase); + var hardwareAccelerationType = options.HardwareAccelerationType; + var supportsKeyFrameOnly = (hardwareAccelerationType == HardwareAccelerationType.nvenc && options.EnableEnhancedNvdecDecoder) + || (hardwareAccelerationType == HardwareAccelerationType.amf && OperatingSystem.IsWindows()) + || (hardwareAccelerationType == HardwareAccelerationType.qsv && options.PreferSystemNativeHwDecoder) + || hardwareAccelerationType == HardwareAccelerationType.vaapi + || hardwareAccelerationType == HardwareAccelerationType.videotoolbox; if (!supportsKeyFrameOnly) { // Disable hardware acceleration when the hardware decoder does not support keyframe only mode. @@ -817,7 +818,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!allowHwAccel) { options.EnableHardwareEncoding = false; - options.HardwareAccelerationType = string.Empty; + options.HardwareAccelerationType = HardwareAccelerationType.none; options.EnableTonemapping = false; } @@ -861,7 +862,7 @@ namespace MediaBrowser.MediaEncoding.Encoder inputArg = "-threads " + threads + " " + inputArg; // HW accel args set a different input thread count, only set if disabled } - if (options.HardwareAccelerationType.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase) && _isLowPriorityHwDecodeSupported) + if (options.HardwareAccelerationType == HardwareAccelerationType.videotoolbox && _isLowPriorityHwDecodeSupported) { // VideoToolbox supports low priority decoding, which is useful for trickplay inputArg = "-hwaccel_flags +low_priority " + inputArg; diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs index 42f355b052..57557d55ca 100644 --- a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs +++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs @@ -352,12 +352,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable { var audioCodec = state.ActualOutputAudioCodec; var videoCodec = state.ActualOutputVideoCodec; - var hardwareAccelerationTypeString = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType; - HardwareEncodingType? hardwareAccelerationType = null; - if (Enum.TryParse(hardwareAccelerationTypeString, out var parsedHardwareAccelerationType)) - { - hardwareAccelerationType = parsedHardwareAccelerationType; - } + var hardwareAccelerationType = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType; _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo { diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 4c5213d4e2..d67a2479fb 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA1819 // XML serialization handles collections improperly, so we need to use arrays + #nullable disable using MediaBrowser.Model.Entities; @@ -30,9 +32,9 @@ public class EncodingOptions EnableTonemapping = false; EnableVppTonemapping = false; EnableVideoToolboxTonemapping = false; - TonemappingAlgorithm = "bt2390"; - TonemappingMode = "auto"; - TonemappingRange = "auto"; + TonemappingAlgorithm = TonemappingAlgorithm.bt2390; + TonemappingMode = TonemappingMode.auto; + TonemappingRange = TonemappingRange.auto; TonemappingDesat = 0; TonemappingPeak = 100; TonemappingParam = 0; @@ -41,7 +43,7 @@ public class EncodingOptions H264Crf = 23; H265Crf = 28; DeinterlaceDoubleRate = false; - DeinterlaceMethod = "yadif"; + DeinterlaceMethod = DeinterlaceMethod.yadif; EnableDecodingColorDepth10Hevc = true; EnableDecodingColorDepth10Vp9 = true; // Enhanced Nvdec or system native decoder is required for DoVi to SDR tone-mapping. @@ -53,8 +55,8 @@ public class EncodingOptions AllowHevcEncoding = false; AllowAv1Encoding = false; EnableSubtitleExtraction = true; - AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" }; - HardwareDecodingCodecs = new string[] { "h264", "vc1" }; + AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = ["mkv"]; + HardwareDecodingCodecs = ["h264", "vc1"]; } /// @@ -120,7 +122,7 @@ public class EncodingOptions /// /// Gets or sets the hardware acceleration type. /// - public string HardwareAccelerationType { get; set; } + public HardwareAccelerationType HardwareAccelerationType { get; set; } /// /// Gets or sets the FFmpeg path as set by the user via the UI. @@ -160,17 +162,17 @@ public class EncodingOptions /// /// Gets or sets the tone-mapping algorithm. /// - public string TonemappingAlgorithm { get; set; } + public TonemappingAlgorithm TonemappingAlgorithm { get; set; } /// /// Gets or sets the tone-mapping mode. /// - public string TonemappingMode { get; set; } + public TonemappingMode TonemappingMode { get; set; } /// /// Gets or sets the tone-mapping range. /// - public string TonemappingRange { get; set; } + public TonemappingRange TonemappingRange { get; set; } /// /// Gets or sets the tone-mapping desaturation. @@ -210,7 +212,7 @@ public class EncodingOptions /// /// Gets or sets the encoder preset. /// - public string EncoderPreset { get; set; } + public EncoderPreset? EncoderPreset { get; set; } /// /// Gets or sets a value indicating whether the framerate is doubled when deinterlacing. @@ -220,7 +222,7 @@ public class EncodingOptions /// /// Gets or sets the deinterlace method. /// - public string DeinterlaceMethod { get; set; } + public DeinterlaceMethod DeinterlaceMethod { get; set; } /// /// Gets or sets a value indicating whether 10bit HEVC decoding is enabled. diff --git a/MediaBrowser.Model/Entities/DeinterlaceMethod.cs b/MediaBrowser.Model/Entities/DeinterlaceMethod.cs new file mode 100644 index 0000000000..d05aac4339 --- /dev/null +++ b/MediaBrowser.Model/Entities/DeinterlaceMethod.cs @@ -0,0 +1,19 @@ +#pragma warning disable SA1300 // Lowercase required for backwards compat. + +namespace MediaBrowser.Model.Entities; + +/// +/// Enum containing deinterlace methods. +/// +public enum DeinterlaceMethod +{ + /// + /// YADIF. + /// + yadif = 0, + + /// + /// BWDIF. + /// + bwdif = 1 +} diff --git a/MediaBrowser.Model/Entities/EncoderPreset.cs b/MediaBrowser.Model/Entities/EncoderPreset.cs new file mode 100644 index 0000000000..74c0714334 --- /dev/null +++ b/MediaBrowser.Model/Entities/EncoderPreset.cs @@ -0,0 +1,64 @@ +#pragma warning disable SA1300 // Lowercase required for backwards compat. + +namespace MediaBrowser.Model.Entities; + +/// +/// Enum containing encoder presets. +/// +public enum EncoderPreset +{ + /// + /// Auto preset. + /// + auto = 0, + + /// + /// Placebo preset. + /// + placebo = 1, + + /// + /// Veryslow preset. + /// + veryslow = 2, + + /// + /// Slower preset. + /// + slower = 3, + + /// + /// Slow preset. + /// + slow = 4, + + /// + /// Medium preset. + /// + medium = 5, + + /// + /// Fast preset. + /// + fast = 6, + + /// + /// Faster preset. + /// + faster = 7, + + /// + /// Veryfast preset. + /// + veryfast = 8, + + /// + /// Superfast preset. + /// + superfast = 9, + + /// + /// Ultrafast preset. + /// + ultrafast = 10 +} diff --git a/MediaBrowser.Model/Entities/HardwareAccelerationType.cs b/MediaBrowser.Model/Entities/HardwareAccelerationType.cs new file mode 100644 index 0000000000..198a2e00f6 --- /dev/null +++ b/MediaBrowser.Model/Entities/HardwareAccelerationType.cs @@ -0,0 +1,49 @@ +#pragma warning disable SA1300 // Lowercase required for backwards compat. + +namespace MediaBrowser.Model.Entities; + +/// +/// Enum containing hardware acceleration types. +/// +public enum HardwareAccelerationType +{ + /// + /// Software accelleration. + /// + none = 0, + + /// + /// AMD AMF. + /// + amf = 1, + + /// + /// Intel Quick Sync Video. + /// + qsv = 2, + + /// + /// NVIDIA NVENC. + /// + nvenc = 3, + + /// + /// Video4Linux2 V4L2M2M. + /// + v4l2m2m = 4, + + /// + /// Video Acceleration API (VAAPI). + /// + vaapi = 5, + + /// + /// Video ToolBox. + /// + videotoolbox = 6, + + /// + /// Rockchip Media Process Platform (RKMPP). + /// + rkmpp = 7 +} diff --git a/MediaBrowser.Model/Entities/TonemappingAlgorithm.cs b/MediaBrowser.Model/Entities/TonemappingAlgorithm.cs new file mode 100644 index 0000000000..488006e0bc --- /dev/null +++ b/MediaBrowser.Model/Entities/TonemappingAlgorithm.cs @@ -0,0 +1,49 @@ +#pragma warning disable SA1300 // Lowercase required for backwards compat. + +namespace MediaBrowser.Model.Entities; + +/// +/// Enum containing tonemapping algorithms. +/// +public enum TonemappingAlgorithm +{ + /// + /// None. + /// + none = 0, + + /// + /// Clip. + /// + clip = 1, + + /// + /// Linear. + /// + linear = 2, + + /// + /// Gamma. + /// + gamma = 3, + + /// + /// Reinhard. + /// + reinhard = 4, + + /// + /// Hable. + /// + hable = 5, + + /// + /// Mobius. + /// + mobius = 6, + + /// + /// BT2390. + /// + bt2390 = 7 +} diff --git a/MediaBrowser.Model/Entities/TonemappingMode.cs b/MediaBrowser.Model/Entities/TonemappingMode.cs new file mode 100644 index 0000000000..e10a0b4ad1 --- /dev/null +++ b/MediaBrowser.Model/Entities/TonemappingMode.cs @@ -0,0 +1,34 @@ +#pragma warning disable SA1300 // Lowercase required for backwards compat. + +namespace MediaBrowser.Model.Entities; + +/// +/// Enum containing tonemapping modes. +/// +public enum TonemappingMode +{ + /// + /// Auto. + /// + auto = 0, + + /// + /// Max. + /// + max = 1, + + /// + /// RGB. + /// + rgb = 2, + + /// + /// Lum. + /// + lum = 3, + + /// + /// ITP. + /// + itp = 4 +} diff --git a/MediaBrowser.Model/Entities/TonemappingRange.cs b/MediaBrowser.Model/Entities/TonemappingRange.cs new file mode 100644 index 0000000000..b1446b81c6 --- /dev/null +++ b/MediaBrowser.Model/Entities/TonemappingRange.cs @@ -0,0 +1,24 @@ +#pragma warning disable SA1300 // Lowercase required for backwards compat. + +namespace MediaBrowser.Model.Entities; + +/// +/// Enum containing tonemapping ranges. +/// +public enum TonemappingRange +{ + /// + /// Auto. + /// + auto = 0, + + /// + /// TV. + /// + tv = 1, + + /// + /// PC. + /// + pc = 2 +} diff --git a/MediaBrowser.Model/Session/HardwareEncodingType.cs b/MediaBrowser.Model/Session/HardwareEncodingType.cs deleted file mode 100644 index cf424fef53..0000000000 --- a/MediaBrowser.Model/Session/HardwareEncodingType.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace MediaBrowser.Model.Session -{ - /// - /// Enum HardwareEncodingType. - /// - public enum HardwareEncodingType - { - /// - /// AMD AMF. - /// - AMF = 0, - - /// - /// Intel Quick Sync Video. - /// - QSV = 1, - - /// - /// NVIDIA NVENC. - /// - NVENC = 2, - - /// - /// Video4Linux2 V4L2. - /// - V4L2M2M = 3, - - /// - /// Video Acceleration API (VAAPI). - /// - VAAPI = 4, - - /// - /// Video ToolBox. - /// - VideoToolBox = 5, - - /// - /// Rockchip Media Process Platform (RKMPP). - /// - RKMPP = 6 - } -} diff --git a/MediaBrowser.Model/Session/TranscodingInfo.cs b/MediaBrowser.Model/Session/TranscodingInfo.cs index 000cbd4c54..ae25267aca 100644 --- a/MediaBrowser.Model/Session/TranscodingInfo.cs +++ b/MediaBrowser.Model/Session/TranscodingInfo.cs @@ -1,34 +1,76 @@ #nullable disable -#pragma warning disable CS1591 -namespace MediaBrowser.Model.Session +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Model.Session; + +/// +/// Class holding information on a runnning transcode. +/// +public class TranscodingInfo { - public class TranscodingInfo - { - public string AudioCodec { get; set; } + /// + /// Gets or sets the thread count used for encoding. + /// + public string AudioCodec { get; set; } - public string VideoCodec { get; set; } + /// + /// Gets or sets the thread count used for encoding. + /// + public string VideoCodec { get; set; } - public string Container { get; set; } + /// + /// Gets or sets the thread count used for encoding. + /// + public string Container { get; set; } - public bool IsVideoDirect { get; set; } + /// + /// Gets or sets a value indicating whether the video is passed through. + /// + public bool IsVideoDirect { get; set; } - public bool IsAudioDirect { get; set; } + /// + /// Gets or sets a value indicating whether the audio is passed through. + /// + public bool IsAudioDirect { get; set; } - public int? Bitrate { get; set; } + /// + /// Gets or sets the bitrate. + /// + public int? Bitrate { get; set; } - public float? Framerate { get; set; } + /// + /// Gets or sets the framerate. + /// + public float? Framerate { get; set; } - public double? CompletionPercentage { get; set; } + /// + /// Gets or sets the completion percentage. + /// + public double? CompletionPercentage { get; set; } - public int? Width { get; set; } + /// + /// Gets or sets the video width. + /// + public int? Width { get; set; } - public int? Height { get; set; } + /// + /// Gets or sets the video height. + /// + public int? Height { get; set; } - public int? AudioChannels { get; set; } + /// + /// Gets or sets the audio channels. + /// + public int? AudioChannels { get; set; } - public HardwareEncodingType? HardwareAccelerationType { get; set; } + /// + /// Gets or sets the hardware acceleration type. + /// + public HardwareAccelerationType? HardwareAccelerationType { get; set; } - public TranscodeReason TranscodeReasons { get; set; } - } + /// + /// Gets or sets the transcode reasons. + /// + public TranscodeReason TranscodeReasons { get; set; } } diff --git a/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs b/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs index 9a023d7ed9..1846ba26bf 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs +++ b/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs @@ -128,7 +128,7 @@ public class DynamicHlsPlaylistGenerator : IDynamicHlsPlaylistGenerator return false; } - internal static bool IsExtractionAllowedForFile(ReadOnlySpan filePath, string[] allowedExtensions) + internal static bool IsExtractionAllowedForFile(ReadOnlySpan filePath, IReadOnlyList allowedExtensions) { var extension = Path.GetExtension(filePath); if (extension.IsEmpty) @@ -138,7 +138,7 @@ public class DynamicHlsPlaylistGenerator : IDynamicHlsPlaylistGenerator // Remove the leading dot var extensionWithoutDot = extension[1..]; - for (var i = 0; i < allowedExtensions.Length; i++) + for (var i = 0; i < allowedExtensions.Count; i++) { var allowedExtension = allowedExtensions[i].AsSpan().TrimStart('.'); if (extensionWithoutDot.Equals(allowedExtension, StringComparison.OrdinalIgnoreCase)) From 36d934f4c0df7abf5e0ab5a8d141c68f1a61b7b0 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Mon, 9 Sep 2024 23:24:45 +0800 Subject: [PATCH 024/159] Enable Rockchip MJPEG encoder for Trickplay (#12610) --- .../MediaEncoding/EncodingHelper.cs | 22 ++++++++------ .../Encoder/EncoderValidator.cs | 3 +- .../Encoder/MediaEncoder.cs | 29 ++++++++++++------- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index fdc56652ae..778c32c4b4 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -129,7 +129,8 @@ namespace MediaBrowser.Controller.MediaEncoding { { HardwareAccelerationType.vaapi, _defaultMjpegEncoder + "_vaapi" }, { HardwareAccelerationType.qsv, _defaultMjpegEncoder + "_qsv" }, - { HardwareAccelerationType.videotoolbox, _defaultMjpegEncoder + "_videotoolbox" } + { HardwareAccelerationType.videotoolbox, _defaultMjpegEncoder + "_videotoolbox" }, + { HardwareAccelerationType.rkmpp, _defaultMjpegEncoder + "_rkmpp" } }; public static readonly string[] LosslessAudioCodecs = @@ -5435,6 +5436,9 @@ namespace MediaBrowser.Controller.MediaEncoding var isSwDecoder = !isRkmppDecoder; var isSwEncoder = !isRkmppEncoder; var isDrmInDrmOut = isRkmppDecoder && isRkmppEncoder; + var isEncoderSupportAfbc = isRkmppEncoder + && (vidEncoder.Contains("h264", StringComparison.OrdinalIgnoreCase) + || vidEncoder.Contains("hevc", StringComparison.OrdinalIgnoreCase)); var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); @@ -5493,7 +5497,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // INPUT rkmpp/drm surface(gem/dma-heap) - var isFullAfbcPipeline = isDrmInDrmOut && !doOclTonemap; + var isFullAfbcPipeline = isEncoderSupportAfbc && isDrmInDrmOut && !doOclTonemap; var swapOutputWandH = doRkVppTranspose && swapWAndH; var outFormat = doOclTonemap ? "p010" : "nv12"; var hwScalePrefix = doRkVppTranspose ? "vpp" : "scale"; @@ -5531,12 +5535,6 @@ namespace MediaBrowser.Controller.MediaEncoding if (doOclTonemap) { var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); - // enable tradeoffs for performance - if (!string.IsNullOrEmpty(tonemapFilter)) - { - tonemapFilter += ":tradeoff=1"; - } - mainFilters.Add(tonemapFilter); } @@ -5607,7 +5605,13 @@ namespace MediaBrowser.Controller.MediaEncoding subFilters.Add("hwupload=derive_device=rkmpp"); // try enabling AFBC to save DDR bandwidth - overlayFilters.Add("overlay_rkrga=eof_action=pass:repeatlast=0:format=nv12:afbc=1"); + var hwOverlayFilter = "overlay_rkrga=eof_action=pass:repeatlast=0:format=nv12"; + if (isEncoderSupportAfbc) + { + hwOverlayFilter += ":afbc=1"; + } + + overlayFilters.Add(hwOverlayFilter); } } else if (memoryOutput) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 2b6ed8fa09..73585caeb9 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -93,7 +93,8 @@ namespace MediaBrowser.MediaEncoding.Encoder "hevc_videotoolbox", "mjpeg_videotoolbox", "h264_rkmpp", - "hevc_rkmpp" + "hevc_rkmpp", + "mjpeg_rkmpp" }; private static readonly string[] _requiredFilters = new[] diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index caa9cb499d..6f87692521 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -896,21 +896,30 @@ namespace MediaBrowser.MediaEncoding.Encoder throw new InvalidOperationException("Empty or invalid input argument."); } - float? encoderQuality = qualityScale; - if (vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase)) + // ffmpeg qscale is a value from 1-31, with 1 being best quality and 31 being worst + // jpeg quality is a value from 0-100, with 0 being worst quality and 100 being best + var encoderQuality = Math.Clamp(qualityScale ?? 4, 1, 31); + var encoderQualityOption = "-qscale:v "; + + if (vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase) + || vidEncoder.Contains("qsv", StringComparison.OrdinalIgnoreCase)) { - // vaapi's mjpeg encoder uses jpeg quality divided by QP2LAMBDA (118) as input, instead of ffmpeg defined qscale - // ffmpeg qscale is a value from 1-31, with 1 being best quality and 31 being worst - // jpeg quality is a value from 0-100, with 0 being worst quality and 100 being best - encoderQuality = (100 - ((qualityScale - 1) * (100 / 30))) / 118; + // vaapi and qsv's mjpeg encoder use jpeg quality as input, instead of ffmpeg defined qscale + encoderQuality = 100 - ((encoderQuality - 1) * (100 / 30)); + encoderQualityOption = "-global_quality:v "; } if (vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase)) { // videotoolbox's mjpeg encoder uses jpeg quality scaled to QP2LAMBDA (118) instead of ffmpeg defined qscale - // ffmpeg qscale is a value from 1-31, with 1 being best quality and 31 being worst - // jpeg quality is a value from 0-100, with 0 being worst quality and 100 being best - encoderQuality = 118 - ((qualityScale - 1) * (118 / 30)); + encoderQuality = 118 - ((encoderQuality - 1) * (118 / 30)); + } + + if (vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase)) + { + // rkmpp's mjpeg encoder uses jpeg quality as input (max is 99, not 100), instead of ffmpeg defined qscale + encoderQuality = 99 - ((encoderQuality - 1) * (99 / 30)); + encoderQualityOption = "-qp_init:v "; } // Output arguments @@ -926,7 +935,7 @@ namespace MediaBrowser.MediaEncoding.Encoder filterParam, outputThreads.GetValueOrDefault(_threads), vidEncoder, - qualityScale.HasValue ? "-qscale:v " + encoderQuality.Value.ToString(CultureInfo.InvariantCulture) + " " : string.Empty, + encoderQualityOption + encoderQuality + " ", vidEncoder.Contains("videotoolbox", StringComparison.InvariantCultureIgnoreCase) ? "-allow_sw 1 " : string.Empty, // allow_sw fallback for some intel macs "image2", outputPath); From d93eb9a87ed53633c47feef9ac424fc47bb89722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Mon, 9 Sep 2024 06:51:11 +0000 Subject: [PATCH 025/159] Translated using Weblate (Czech) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/cs/ --- Emby.Server.Implementations/Localization/Core/cs.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index ad9e555a3c..ba2e2700da 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -132,5 +132,9 @@ "TaskAudioNormalization": "Normalizace zvuku", "TaskAudioNormalizationDescription": "Skenovat soubory za účelem normalizace zvuku.", "TaskDownloadMissingLyrics": "Stáhnout chybějící texty k písni", - "TaskDownloadMissingLyricsDescription": "Stáhne texty k písni" + "TaskDownloadMissingLyricsDescription": "Stáhne texty k písni", + "TaskExtractMediaSegments": "Skenování segmentů médií", + "TaskExtractMediaSegmentsDescription": "Extrahuje či získá segmenty médií pomocí zásuvných modulů MediaSegment.", + "TaskMoveTrickplayImages": "Přesunout úložiště obrázků Trickplay", + "TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny." } From 98ea585a0f93dd8eda7d671beaf724525233b799 Mon Sep 17 00:00:00 2001 From: Lea3D Date: Mon, 9 Sep 2024 11:07:45 +0000 Subject: [PATCH 026/159] Translated using Weblate (German) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/ --- Emby.Server.Implementations/Localization/Core/de.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index bbb162c776..51c9e87d5a 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -132,5 +132,9 @@ "TaskAudioNormalization": "Audio Normalisierung", "TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten.", "TaskDownloadMissingLyricsDescription": "Lädt Songtexte herunter", - "TaskDownloadMissingLyrics": "Fehlende Songtexte herunterladen" + "TaskDownloadMissingLyrics": "Fehlende Songtexte herunterladen", + "TaskExtractMediaSegments": "Scanne Mediensegmente", + "TaskExtractMediaSegmentsDescription": "Extrahiert oder empfängt Mediensegmente von Plugins die Mediensegmente nutzen.", + "TaskMoveTrickplayImages": "Verzeichnis für Trickplay-Bilder migrieren", + "TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben." } From 0003a55c1dc8e27339db3d0a78a32970e09dac6b Mon Sep 17 00:00:00 2001 From: Federico Abella Date: Sun, 8 Sep 2024 23:53:24 +0000 Subject: [PATCH 027/159] Translated using Weblate (Spanish (Argentina)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_AR/ --- Emby.Server.Implementations/Localization/Core/es-AR.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json index b926d9d30e..f2f657b049 100644 --- a/Emby.Server.Implementations/Localization/Core/es-AR.json +++ b/Emby.Server.Implementations/Localization/Core/es-AR.json @@ -132,5 +132,9 @@ "TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción", "TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen.", "TaskDownloadMissingLyrics": "Descargar letra faltante", - "TaskDownloadMissingLyricsDescription": "Descarga letras de canciones" + "TaskDownloadMissingLyricsDescription": "Descarga letras de canciones", + "TaskExtractMediaSegments": "Escanear Segmentos de Media", + "TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medio de plugins habilitados para MediaSegment.", + "TaskMoveTrickplayImagesDescription": "Mueve archivos existentes de trickplay de acuerdo a la configuración de la biblioteca.", + "TaskMoveTrickplayImages": "Migrar Ubicación de Imagen de Trickplay" } From 9bbb3b61642d9b9500b96ba694f624cae0a60e75 Mon Sep 17 00:00:00 2001 From: Bas <44002186+854562@users.noreply.github.com> Date: Sun, 8 Sep 2024 14:45:25 +0000 Subject: [PATCH 028/159] Translated using Weblate (Dutch) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/ --- Emby.Server.Implementations/Localization/Core/nl.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 39e7cd546d..1522720dc5 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -132,5 +132,9 @@ "TaskAudioNormalization": "Geluidsnormalisatie", "TaskAudioNormalizationDescription": "Scant bestanden op gegevens voor geluidsnormalisatie.", "TaskDownloadMissingLyrics": "Ontbrekende liedteksten downloaden", - "TaskDownloadMissingLyricsDescription": "Downloadt liedteksten" + "TaskDownloadMissingLyricsDescription": "Downloadt liedteksten", + "TaskExtractMediaSegmentsDescription": "Verkrijgt mediasegmenten vanuit plug-ins met MediaSegment-ondersteuning.", + "TaskMoveTrickplayImages": "Locatie trickplay-afbeeldingen migreren", + "TaskMoveTrickplayImagesDescription": "Verplaatst bestaande trickplay-bestanden op basis van de bibliotheekinstellingen.", + "TaskExtractMediaSegments": "Scannen op mediasegmenten" } From 34323ae811283ea93f31babf19ee12c58837705d Mon Sep 17 00:00:00 2001 From: Kityn Date: Mon, 9 Sep 2024 05:40:06 +0000 Subject: [PATCH 029/159] Translated using Weblate (Polish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pl/ --- Emby.Server.Implementations/Localization/Core/pl.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json index a24a837aba..33b0bb7e15 100644 --- a/Emby.Server.Implementations/Localization/Core/pl.json +++ b/Emby.Server.Implementations/Localization/Core/pl.json @@ -132,5 +132,9 @@ "TaskAudioNormalization": "Normalizacja dźwięku", "TaskAudioNormalizationDescription": "Skanuje pliki w poszukiwaniu danych normalizacji dźwięku.", "TaskDownloadMissingLyrics": "Pobierz brakujące słowa", - "TaskDownloadMissingLyricsDescription": "Pobierz słowa piosenek" + "TaskDownloadMissingLyricsDescription": "Pobierz słowa piosenek", + "TaskExtractMediaSegments": "Skanowanie segmentów mediów", + "TaskMoveTrickplayImages": "Migruj lokalizację obrazu Trickplay", + "TaskExtractMediaSegmentsDescription": "Wyodrębnia lub pobiera segmenty mediów z wtyczek obsługujących MediaSegment.", + "TaskMoveTrickplayImagesDescription": "Przenosi istniejące pliki Trickplay zgodnie z ustawieniami biblioteki." } From 43861f0ce112873514d3f97f3c4320525d642b39 Mon Sep 17 00:00:00 2001 From: Jolter Date: Sun, 8 Sep 2024 19:38:23 +0000 Subject: [PATCH 030/159] Translated using Weblate (Swedish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sv/ --- Emby.Server.Implementations/Localization/Core/sv.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index a4e2302d16..5cf54522bf 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -9,7 +9,7 @@ "Channels": "Kanaler", "ChapterNameValue": "Kapitel {0}", "Collections": "Samlingar", - "DeviceOfflineWithName": "{0} har avbrutit uppkopplingen", + "DeviceOfflineWithName": "{0} har kopplat ned", "DeviceOnlineWithName": "{0} är ansluten", "FailedLoginAttemptWithUserName": "Misslyckat inloggningsförsök från {0}", "Favorites": "Favoriter", @@ -121,7 +121,7 @@ "Default": "Standard", "TaskOptimizeDatabase": "Optimera databasen", "TaskOptimizeDatabaseDescription": "Komprimerar databasen och trunkerar ledigt utrymme. Prestandan kan förbättras genom att köra denna aktivitet efter att du har skannat biblioteket eller gjort andra förändringar som indikerar att databasen har modifierats.", - "TaskKeyframeExtractorDescription": "Exporterar nyckelbildrutor från videofiler för att skapa mer exakta HLS-spellistor. Denna rutin kan ta lång tid.", + "TaskKeyframeExtractorDescription": "Exporterar nyckelbildrutor från videofiler för att skapa mer exakta HLS-spellistor. Denna körning kan ta lång tid.", "TaskKeyframeExtractor": "Extraktor för nyckelbildrutor", "External": "Extern", "HearingImpaired": "Hörselskadad", @@ -132,5 +132,9 @@ "TaskCleanCollectionsAndPlaylistsDescription": "Tar bort objekt från samlingar och spellistor som inte längre finns.", "TaskAudioNormalizationDescription": "Skannar filer för ljudnormaliseringsdata.", "TaskDownloadMissingLyrics": "Ladda ner saknad låttext", - "TaskDownloadMissingLyricsDescription": "Laddar ner låttexter" + "TaskDownloadMissingLyricsDescription": "Laddar ner låttexter", + "TaskExtractMediaSegments": "Skanning av mediesegment", + "TaskExtractMediaSegmentsDescription": "Extraherar eller hämtar ut mediesegmen från tillägg som stöder MediaSegment.", + "TaskMoveTrickplayImages": "Migrera platsen för Trickplay-bilder", + "TaskMoveTrickplayImagesDescription": "Flyttar befintliga trickplay-filer enligt bibliotekets inställningar." } From 3da081ba86940f3fcedb188b2243445d1f95c883 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com> Date: Mon, 9 Sep 2024 22:16:58 +0300 Subject: [PATCH 031/159] Add audio ranking for transcoding profiles (#12546) --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 100 ++++++++++++------ .../Dlna/StreamBuilderTests.cs | 3 + ...ceInfo-mp4-h264-ac3-aac-mp3-srt-2600k.json | 100 ++++++++++++++++++ 3 files changed, 172 insertions(+), 31 deletions(-) create mode 100644 tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-mp3-srt-2600k.json diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 490ae4e629..f68a8bca34 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -751,8 +751,9 @@ namespace MediaBrowser.Model.Dlna { // Can't direct play, find the transcoding profile // If we do this for direct-stream we will overwrite the info - var transcodingProfile = GetVideoTranscodeProfile(item, options, videoStream, audioStream, candidateAudioStreams, subtitleStream, playlistItem); - if (transcodingProfile is not null) + var (transcodingProfile, playMethod) = GetVideoTranscodeProfile(item, options, videoStream, audioStream, candidateAudioStreams, subtitleStream, playlistItem); + + if (transcodingProfile is not null && playMethod.HasValue) { SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile); @@ -790,7 +791,7 @@ namespace MediaBrowser.Model.Dlna return playlistItem; } - private TranscodingProfile? GetVideoTranscodeProfile( + private (TranscodingProfile? Profile, PlayMethod? PlayMethod) GetVideoTranscodeProfile( MediaSourceInfo item, MediaOptions options, MediaStream? videoStream, @@ -801,7 +802,7 @@ namespace MediaBrowser.Model.Dlna { if (!(item.SupportsTranscoding || item.SupportsDirectStream)) { - return null; + return (null, null); } var transcodingProfiles = options.Profile.TranscodingProfiles @@ -812,41 +813,78 @@ namespace MediaBrowser.Model.Dlna transcodingProfiles = transcodingProfiles.Where(i => string.Equals(i.Container, "ts", StringComparison.OrdinalIgnoreCase)); } - if (options.AllowVideoStreamCopy) - { - // prefer direct copy profile - float videoFramerate = videoStream?.ReferenceFrameRate ?? 0; - TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp; - int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); - int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); + var videoCodec = videoStream?.Codec; + float videoFramerate = videoStream?.ReferenceFrameRate ?? 0; + TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp; + int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); + int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); - transcodingProfiles = transcodingProfiles.ToLookup(transcodingProfile => + var audioCodec = audioStream?.Codec; + var audioProfile = audioStream?.Profile; + var audioChannels = audioStream?.Channels; + var audioBitrate = audioStream?.BitRate; + var audioSampleRate = audioStream?.SampleRate; + var audioBitDepth = audioStream?.BitDepth; + + var analyzedProfiles = transcodingProfiles + .Select(transcodingProfile => { - var videoCodecs = ContainerProfile.SplitValue(transcodingProfile.VideoCodec); + var rank = (Video: 3, Audio: 3); - if (ContainerProfile.ContainsContainer(videoCodecs, item.VideoStream?.Codec)) + var container = transcodingProfile.Container; + + if (options.AllowVideoStreamCopy) { - var videoCodec = videoStream?.Codec; - var container = transcodingProfile.Container; - var appliedVideoConditions = options.Profile.CodecProfiles - .Where(i => i.Type == CodecType.Video && - i.ContainsAnyCodec(videoCodec, container) && - i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC))) - .Select(i => - i.Conditions.All(condition => ConditionProcessor.IsVideoConditionSatisfied(condition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC))); + var videoCodecs = ContainerProfile.SplitValue(transcodingProfile.VideoCodec); - // An empty appliedVideoConditions means that the codec has no conditions for the current video stream - var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied); - return conditionsSatisfied ? 1 : 2; + if (ContainerProfile.ContainsContainer(videoCodecs, videoCodec)) + { + var appliedVideoConditions = options.Profile.CodecProfiles + .Where(i => i.Type == CodecType.Video && + i.ContainsAnyCodec(videoCodec, container) && + i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC))) + .Select(i => + i.Conditions.All(condition => ConditionProcessor.IsVideoConditionSatisfied(condition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC))); + + // An empty appliedVideoConditions means that the codec has no conditions for the current video stream + var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied); + rank.Video = conditionsSatisfied ? 1 : 2; + } } - return 3; - }) - .OrderBy(lookup => lookup.Key) - .SelectMany(lookup => lookup); - } + if (options.AllowAudioStreamCopy) + { + var audioCodecs = ContainerProfile.SplitValue(transcodingProfile.AudioCodec); - return transcodingProfiles.FirstOrDefault(); + if (ContainerProfile.ContainsContainer(audioCodecs, audioCodec)) + { + var appliedVideoConditions = options.Profile.CodecProfiles + .Where(i => i.Type == CodecType.VideoAudio && + i.ContainsAnyCodec(audioCodec, container) && + i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false))) + .Select(i => + i.Conditions.All(condition => ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false))); + + // An empty appliedVideoConditions means that the codec has no conditions for the current audio stream + var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied); + rank.Audio = conditionsSatisfied ? 1 : 2; + } + } + + PlayMethod playMethod = PlayMethod.Transcode; + + if (rank.Video == 1) + { + playMethod = PlayMethod.DirectStream; + } + + return (Profile: transcodingProfile, PlayMethod: playMethod, Rank: rank); + }) + .OrderBy(analysis => analysis.Rank); + + var profileMatch = analyzedProfiles.FirstOrDefault(); + + return (profileMatch.Profile, profileMatch.PlayMethod); } private void BuildStreamVideoItem( diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 31ddd427cc..3429d1a5bd 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -309,6 +309,9 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] [InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] + // TranscodeMedia + [InlineData("TranscodeMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")] + [InlineData("TranscodeMedia", "mp4-h264-ac3-aac-mp3-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.ts")] public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = default, string transcodeMode = "DirectStream", string transcodeProtocol = "") { var options = await GetMediaOptions(deviceName, mediaSource); diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-mp3-srt-2600k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-mp3-srt-2600k.json new file mode 100644 index 0000000000..2e05e70d69 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-mp3-srt-2600k.json @@ -0,0 +1,100 @@ +{ + "Id": "a766d122b58e45d9492d17af77748bf5", + "Path": "/Media/MyVideo-720p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 835317696, + "Name": "MyVideo-720p", + "ETag": "579a34c6d5dfb21d81539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "High", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 41 + }, + { + "Codec": "ac3", + "CodecTag": "ac-3", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - Dolby Digital - 5.1 - Default", + "ChannelLayout": "5.1", + "BitRate": 384000, + "Channels": 6, + "SampleRate": 48000, + "IsDefault": true, + "Index": 1, + "Score": 202 + }, + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - AAC - Stereo", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": false, + "Profile": "LC", + "Index": 2, + "Score": 203 + }, + { + "Codec": "mp3", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - MP3 - Stereo", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": false, + "Index": 3, + "Score": 203 + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 4, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt" + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 4 +} From 987dbe98c8ab55c5c8eb563820e54453c835cdde Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 10 Sep 2024 03:17:10 +0800 Subject: [PATCH 032/159] cli: add option to disable network change detection (#11253) --- Emby.Server.Implementations/ConfigurationOptions.cs | 3 ++- Jellyfin.Server/StartupOptions.cs | 11 +++++++++++ .../Extensions/ConfigurationExtensions.cs | 5 +++++ src/Jellyfin.Networking/Manager/NetworkManager.cs | 9 +++++++-- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index e860105133..702707297a 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -20,7 +20,8 @@ namespace Emby.Server.Implementations { PlaylistsAllowDuplicatesKey, bool.FalseString }, { BindToUnixSocketKey, bool.FalseString }, { SqliteCacheSizeKey, "20000" }, - { FfmpegSkipValidationKey, bool.FalseString } + { FfmpegSkipValidationKey, bool.FalseString }, + { DetectNetworkChangeKey, bool.TrueString } }; } } diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index c3989751ca..91ac827ca6 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -67,6 +67,12 @@ namespace Jellyfin.Server [Option("published-server-url", Required = false, HelpText = "Jellyfin Server URL to publish via auto discover process")] public string? PublishedServerUrl { get; set; } + /// + /// Gets or sets a value indicating whether the server should not detect network status change. + /// + [Option("nonetchange", Required = false, HelpText = "Indicates that the server should not detect network status change.")] + public bool NoDetectNetworkChange { get; set; } + /// /// Gets the command line options as a dictionary that can be used in the .NET configuration system. /// @@ -90,6 +96,11 @@ namespace Jellyfin.Server config.Add(FfmpegPathKey, FFmpegPath); } + if (NoDetectNetworkChange) + { + config.Add(DetectNetworkChangeKey, bool.FalseString); + } + return config; } } diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index 0aaf4fcd90..7ca5084266 100644 --- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -69,6 +69,11 @@ namespace MediaBrowser.Controller.Extensions /// public const string SqliteCacheSizeKey = "sqlite:cacheSize"; + /// + /// The key for a setting that indicates whether the application should detect network status change. + /// + public const string DetectNetworkChangeKey = "DetectNetworkChange"; + /// /// Gets a value indicating whether the application should host static web content from the . /// diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index cf6a2cc553..b285b836bc 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -97,10 +97,15 @@ public class NetworkManager : INetworkManager, IDisposable _networkEventLock = new object(); _remoteAddressFilter = new List(); + _ = bool.TryParse(startupConfig[DetectNetworkChangeKey], out var detectNetworkChange); + UpdateSettings(_configurationManager.GetNetworkConfiguration()); - NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged; - NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; + if (detectNetworkChange) + { + NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged; + NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; + } _configurationManager.NamedConfigurationUpdated += ConfigurationUpdated; } From c14b5306928f489cba15562f13421026f5aee561 Mon Sep 17 00:00:00 2001 From: Hoomaane79 Date: Mon, 9 Sep 2024 18:55:47 +0000 Subject: [PATCH 033/159] Translated using Weblate (Persian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fa/ --- Emby.Server.Implementations/Localization/Core/fa.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json index b0ddec1046..ff14c13678 100644 --- a/Emby.Server.Implementations/Localization/Core/fa.json +++ b/Emby.Server.Implementations/Localization/Core/fa.json @@ -132,5 +132,9 @@ "TaskAudioNormalizationDescription": "بررسی فایل برای داده‌های نرمال کردن صدا.", "TaskDownloadMissingLyrics": "دانلود متن‌های ناموجود", "TaskDownloadMissingLyricsDescription": "دانلود متن شعر‌ها", - "TaskAudioNormalization": "نرمال کردن صدا" + "TaskAudioNormalization": "نرمال کردن صدا", + "TaskExtractMediaSegments": "بررسی بخش محتوا", + "TaskExtractMediaSegmentsDescription": "بخش‌های محتوا را از افزونه‌های مربوط استخراح می‌کند.", + "TaskMoveTrickplayImages": "جابه‌جایی عکس‌های Trickplay", + "TaskMoveTrickplayImagesDescription": "داده‌های Trickplay را با توجه به تنظیمات کتاب‌خانه جابه‌جا می‌کند." } From 7d2a498f68c758347fc0437e142c476353aed783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=84=A1=E6=83=85=E5=A4=A9?= Date: Mon, 9 Sep 2024 16:46:17 +0000 Subject: [PATCH 034/159] Translated using Weblate (Chinese (Simplified)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/ --- Emby.Server.Implementations/Localization/Core/zh-CN.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index 4bec590fbe..a2337e0690 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -132,5 +132,9 @@ "TaskAudioNormalization": "音频标准化", "TaskAudioNormalizationDescription": "扫描文件以寻找音频标准化数据。", "TaskDownloadMissingLyrics": "下载缺失的歌词", - "TaskDownloadMissingLyricsDescription": "下载歌曲歌词" + "TaskDownloadMissingLyricsDescription": "下载歌曲歌词", + "TaskMoveTrickplayImages": "迁移 特技播放 图像位置", + "TaskExtractMediaSegments": "媒体片段扫描", + "TaskExtractMediaSegmentsDescription": "从启用 MediaSegment 的插件中提取或获取媒体片段。", + "TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的特技播放文件。" } From c67b78bc68317c66f3324230b2fc36918b2b9d70 Mon Sep 17 00:00:00 2001 From: stanol Date: Tue, 10 Sep 2024 14:51:15 +0000 Subject: [PATCH 035/159] Translated using Weblate (Ukrainian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/uk/ --- Emby.Server.Implementations/Localization/Core/uk.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index 97bad45323..3fddc2e780 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -131,5 +131,9 @@ "TaskAudioNormalizationDescription": "Сканує файли на наявність даних для нормалізації звуку.", "TaskAudioNormalization": "Нормалізація аудіо", "TaskDownloadMissingLyrics": "Завантажити відсутні тексти пісень", - "TaskDownloadMissingLyricsDescription": "Завантаження текстів пісень" + "TaskDownloadMissingLyricsDescription": "Завантаження текстів пісень", + "TaskMoveTrickplayImagesDescription": "Переміщує наявні Trickplay-зображення відповідно до налаштувань медіатеки.", + "TaskExtractMediaSegments": "Сканування медіа-сегментів", + "TaskMoveTrickplayImages": "Змінити місце розташування Trickplay-зображень", + "TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment." } From 7c3c0aa940b29cba64ba241eee3cdcd7f65a9eb6 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Tue, 23 Jul 2024 15:47:38 -0700 Subject: [PATCH 036/159] Use subtitle cache when burning-in subs --- .../MediaEncoding/EncodingHelper.cs | 10 ++++++---- .../MediaEncoding/ISubtitleEncoder.cs | 9 +++++++++ .../Subtitles/SubtitleEncoder.cs | 6 ++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 778c32c4b4..5cc604a027 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1652,13 +1652,15 @@ namespace MediaBrowser.Controller.MediaEncoding setPtsParam); } - var mediaPath = state.MediaPath ?? string.Empty; + var subtitlePath = _subtitleEncoder.GetSubtitleFilePath( + state.SubtitleStream, + state.MediaSource, + CancellationToken.None).GetAwaiter().GetResult(); return string.Format( CultureInfo.InvariantCulture, - "subtitles=f='{0}':si={1}{2}{3}{4}{5}", - _mediaEncoder.EscapeSubtitleFilterPath(mediaPath), - state.InternalSubtitleStreamOffset.ToString(CultureInfo.InvariantCulture), + "subtitles=f='{0}'{1}{2}{3}{4}", + _mediaEncoder.EscapeSubtitleFilterPath(subtitlePath), alphaParam, sub2videoParam, fontParam, diff --git a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs index 5bf83a9e31..9bf27b3b2e 100644 --- a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs @@ -44,5 +44,14 @@ namespace MediaBrowser.Controller.MediaEncoding /// The cancellation token. /// System.String. Task GetSubtitleFileCharacterSet(MediaStream subtitleStream, string language, MediaSourceInfo mediaSource, CancellationToken cancellationToken); + + /// + /// Gets the path to a subtitle file. + /// + /// The subtitle stream. + /// The media source. + /// The cancellation token. + /// System.String. + Task GetSubtitleFilePath(MediaStream subtitleStream, MediaSourceInfo mediaSource, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index dbb4b823e3..88a1111a07 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -902,6 +902,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } + public async Task GetSubtitleFilePath(MediaStream subtitleStream, MediaSourceInfo mediaSource, CancellationToken cancellationToken) + { + var info = await GetReadableFile(mediaSource, subtitleStream, cancellationToken); + return info.Path; + } + /// public void Dispose() { From f38e715d01c6ff362524af42a267bfcd9659a0ea Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Fri, 2 Aug 2024 01:00:04 -0700 Subject: [PATCH 037/159] Add @heartles to CONTRIBUTORS.md --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 91faa2c2e1..c262a313c1 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -189,6 +189,7 @@ - [TheMelmacian](https://github.com/TheMelmacian) - [ItsAllAboutTheCode](https://github.com/ItsAllAboutTheCode) - [pret0rian8](https://github.com/pret0rian) + - [jaina heartles](https://github.com/heartles) # Emby Contributors From d2c2dcd53c24938c59061de4f826f59e67d8ff15 Mon Sep 17 00:00:00 2001 From: oxixes <38050447+oxixes@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:43:02 +0200 Subject: [PATCH 038/159] Solve CodeQL issue --- MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 88a1111a07..a731d4785b 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -904,7 +904,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles public async Task GetSubtitleFilePath(MediaStream subtitleStream, MediaSourceInfo mediaSource, CancellationToken cancellationToken) { - var info = await GetReadableFile(mediaSource, subtitleStream, cancellationToken); + var info = await GetReadableFile(mediaSource, subtitleStream, cancellationToken) + .ConfigureAwait(false); return info.Path; } From dd462f807268a6c3eb4b053605c11316b1795b98 Mon Sep 17 00:00:00 2001 From: oxixes <38050447+oxixes@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:50:51 +0200 Subject: [PATCH 039/159] Add @oxixes to contributors --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index c262a313c1..5b94e04e15 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -190,6 +190,7 @@ - [ItsAllAboutTheCode](https://github.com/ItsAllAboutTheCode) - [pret0rian8](https://github.com/pret0rian) - [jaina heartles](https://github.com/heartles) + - [oxixes](https://github.com/oxixes) # Emby Contributors From 81aca67745d541e276cfa05efe57e52e9f5a5d0e Mon Sep 17 00:00:00 2001 From: Gary Wang Date: Wed, 11 Sep 2024 23:36:56 +0800 Subject: [PATCH 040/159] feat(i18n): able to finetune transliterator (#12378) --- src/Jellyfin.Extensions/StringExtensions.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs index 8cfebd594d..4b9677d9f4 100644 --- a/src/Jellyfin.Extensions/StringExtensions.cs +++ b/src/Jellyfin.Extensions/StringExtensions.cs @@ -9,8 +9,21 @@ namespace Jellyfin.Extensions /// public static partial class StringExtensions { - private static readonly Lazy _transliterator = new(() => Transliterator.GetInstance( - "Any-Latin; Latin-Ascii; Lower; NFD; [:Nonspacing Mark:] Remove; [:Punctuation:] Remove;")); + private static readonly Lazy _transliteratorId = new(() => + Environment.GetEnvironmentVariable("JELLYFIN_TRANSLITERATOR_ID") + ?? "Any-Latin; Latin-Ascii; Lower; NFD; [:Nonspacing Mark:] Remove; [:Punctuation:] Remove;"); + + private static readonly Lazy _transliterator = new(() => + { + try + { + return Transliterator.GetInstance(_transliteratorId.Value); + } + catch (ArgumentException) + { + return null; + } + }); // Matches non-conforming unicode chars // https://mnaoumov.wordpress.com/2014/06/14/stripping-invalid-characters-from-utf-16-strings/ @@ -108,7 +121,7 @@ namespace Jellyfin.Extensions /// The transliterated string. public static string Transliterated(this string text) { - return _transliterator.Value.Transliterate(text); + return (_transliterator.Value is null) ? text : _transliterator.Value.Transliterate(text); } } } From 624800a1c79b4e272ade89a73790579f6ecbe115 Mon Sep 17 00:00:00 2001 From: Andi Chandler Date: Tue, 10 Sep 2024 20:28:58 +0000 Subject: [PATCH 041/159] Translated using Weblate (English (United Kingdom)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/en_GB/ --- Emby.Server.Implementations/Localization/Core/en-GB.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json index 65df1e45b1..ca52ffb143 100644 --- a/Emby.Server.Implementations/Localization/Core/en-GB.json +++ b/Emby.Server.Implementations/Localization/Core/en-GB.json @@ -132,5 +132,9 @@ "TaskAudioNormalization": "Audio Normalisation", "TaskAudioNormalizationDescription": "Scans files for audio normalisation data.", "TaskDownloadMissingLyrics": "Download missing lyrics", - "TaskDownloadMissingLyricsDescription": "Downloads lyrics for songs" + "TaskDownloadMissingLyricsDescription": "Downloads lyrics for songs", + "TaskExtractMediaSegments": "Media Segment Scan", + "TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.", + "TaskMoveTrickplayImages": "Migrate Trickplay Image Location", + "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings." } From 23590bb962c553758194c06e4585c5ee64189540 Mon Sep 17 00:00:00 2001 From: nextlooper42 Date: Wed, 11 Sep 2024 09:34:29 +0000 Subject: [PATCH 042/159] Translated using Weblate (Slovak) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sk/ --- Emby.Server.Implementations/Localization/Core/sk.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index a9b6fbeef4..66d8bf899b 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -130,5 +130,11 @@ "TaskCleanCollectionsAndPlaylists": "Vyčistiť kolekcie a playlisty", "TaskCleanCollectionsAndPlaylistsDescription": "Odstráni položky z kolekcií a playlistov, ktoré už neexistujú.", "TaskAudioNormalization": "Normalizácia zvuku", - "TaskAudioNormalizationDescription": "Skenovať súbory za účelom normalizácie zvuku." + "TaskAudioNormalizationDescription": "Skenovať súbory za účelom normalizácie zvuku.", + "TaskExtractMediaSegments": "Skenovanie segmentov médií", + "TaskExtractMediaSegmentsDescription": "Extrahuje alebo získava segmenty médií zo zásuvných modulov s povolenou funkciou MediaSegment.", + "TaskMoveTrickplayImages": "Presunúť umiestnenie obrázkov Trickplay", + "TaskMoveTrickplayImagesDescription": "Presunie existujúce súbory Trickplay podľa nastavení knižnice.", + "TaskDownloadMissingLyrics": "Stiahnuť chýbajúce texty piesní", + "TaskDownloadMissingLyricsDescription": "Stiahne texty pre piesne" } From fdb3f3c7b760a8e0319645b34fcafce58e75e9f9 Mon Sep 17 00:00:00 2001 From: queeup Date: Tue, 10 Sep 2024 21:14:02 +0000 Subject: [PATCH 043/159] Translated using Weblate (Turkish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/tr/ --- Emby.Server.Implementations/Localization/Core/tr.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index 1dceadc611..a3cf78fcb2 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -130,5 +130,11 @@ "TaskCleanCollectionsAndPlaylistsDescription": "Artık var olmayan koleksiyon ve çalma listelerindeki ögeleri kaldırır.", "TaskCleanCollectionsAndPlaylists": "Koleksiyonları ve çalma listelerini temizleyin", "TaskAudioNormalizationDescription": "Ses normalleştirme verileri için dosyaları tarar.", - "TaskAudioNormalization": "Ses Normalleştirme" + "TaskAudioNormalization": "Ses Normalleştirme", + "TaskExtractMediaSegments": "Medya Segmenti Tarama", + "TaskMoveTrickplayImages": "Trickplay Görsel Konumunu Taşıma", + "TaskMoveTrickplayImagesDescription": "Mevcut trickplay dosyalarını kütüphane ayarlarına göre taşır.", + "TaskDownloadMissingLyrics": "Eksik şarkı sözlerini indir", + "TaskDownloadMissingLyricsDescription": "Şarkı sözlerini indirir", + "TaskExtractMediaSegmentsDescription": "MediaSegment özelliği etkin olan eklentilerden medya segmentlerini çıkarır veya alır." } From 074d9aa5d5cb191e7baca669dfe86314d229ca5a Mon Sep 17 00:00:00 2001 From: Nyanmisaka <799610810@qq.com> Date: Wed, 11 Sep 2024 18:55:22 +0000 Subject: [PATCH 044/159] Translated using Weblate (Chinese (Simplified)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/ --- Emby.Server.Implementations/Localization/Core/zh-CN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index a2337e0690..cbec0979ac 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -133,8 +133,8 @@ "TaskAudioNormalizationDescription": "扫描文件以寻找音频标准化数据。", "TaskDownloadMissingLyrics": "下载缺失的歌词", "TaskDownloadMissingLyricsDescription": "下载歌曲歌词", - "TaskMoveTrickplayImages": "迁移 特技播放 图像位置", + "TaskMoveTrickplayImages": "迁移时间轴缩略图的存储位置", "TaskExtractMediaSegments": "媒体片段扫描", - "TaskExtractMediaSegmentsDescription": "从启用 MediaSegment 的插件中提取或获取媒体片段。", + "TaskExtractMediaSegmentsDescription": "从支持 MediaSegment 的插件中提取或获取媒体片段。", "TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的特技播放文件。" } From 751e12e5b5c59f9df4494251166f47c8cf9ebfe1 Mon Sep 17 00:00:00 2001 From: felix920506 Date: Tue, 10 Sep 2024 22:07:04 +0000 Subject: [PATCH 045/159] Translated using Weblate (Chinese (Traditional)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant/ --- Emby.Server.Implementations/Localization/Core/zh-TW.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index f06bbc5912..81d5b83d61 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -129,5 +129,11 @@ "TaskCleanCollectionsAndPlaylists": "清理系列和播放清單", "TaskCleanCollectionsAndPlaylistsDescription": "清理系列和播放清單中已不存在的項目。", "TaskAudioNormalization": "音量標準化", - "TaskAudioNormalizationDescription": "掃描文件以找出音量標準化資料。" + "TaskAudioNormalizationDescription": "掃描文件以找出音量標準化資料。", + "TaskDownloadMissingLyrics": "下載缺少的歌詞", + "TaskDownloadMissingLyricsDescription": "卡在歌曲歌詞", + "TaskExtractMediaSegments": "掃描媒體片段", + "TaskExtractMediaSegmentsDescription": "從使用媒體片段的擴充功能取得媒體片段。", + "TaskMoveTrickplayImages": "遷移快轉縮圖位置", + "TaskMoveTrickplayImagesDescription": "根據媒體庫的設定遷移快轉縮圖的檔案。" } From 6deebb449860c67f3b77ec4041efeaa217a18bb9 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Thu, 12 Sep 2024 23:52:03 +0800 Subject: [PATCH 046/159] Fix QSV presets may be empty (#12633) --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 5cc604a027..bf6d47ba12 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1491,14 +1491,7 @@ namespace MediaBrowser.Controller.MediaEncoding { EncoderPreset[] valid_presets = [EncoderPreset.veryslow, EncoderPreset.slower, EncoderPreset.slow, EncoderPreset.medium, EncoderPreset.fast, EncoderPreset.faster, EncoderPreset.veryfast]; - if (valid_presets.Contains(encoderPreset)) - { - param += " -preset " + encodingOptions.EncoderPreset; - } - else - { - param += " -preset " + EncoderPreset.veryfast.ToString().ToLowerInvariant(); - } + param += " -preset " + (valid_presets.Contains(encoderPreset) ? encoderPreset : EncoderPreset.veryfast).ToString().ToLowerInvariant(); } else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc) || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) // hevc (hevc_nvenc) From 0ff7f28753ed4848b0d5cdbe615787bcf7f3426b Mon Sep 17 00:00:00 2001 From: gnattu Date: Thu, 12 Sep 2024 23:52:24 +0800 Subject: [PATCH 047/159] Enable BWDIF VideoToolbox deint filter when available (#12634) --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 5 ++++- MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index bf6d47ba12..5a4af8ce88 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -3237,9 +3237,12 @@ namespace MediaBrowser.Controller.MediaEncoding if (hwDeintSuffix.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase)) { + var useBwdif = options.DeinterlaceMethod == DeinterlaceMethod.bwdif && _mediaEncoder.SupportsFilter("bwdif_videotoolbox"); + return string.Format( CultureInfo.InvariantCulture, - "yadif_videotoolbox={0}:-1:0", + "{0}_videotoolbox={1}:-1:0", + useBwdif ? "bwdif" : "yadif", doubleRateDeint ? "1" : "0"); } diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 73585caeb9..b49fbf2aba 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -137,6 +137,7 @@ namespace MediaBrowser.MediaEncoding.Encoder "flip_vulkan", // videotoolbox "yadif_videotoolbox", + "bwdif_videotoolbox", "scale_vt", "transpose_vt", "overlay_videotoolbox", From 62712aa12cdc88c524923cb318783b23a8a7a2c5 Mon Sep 17 00:00:00 2001 From: gnattu Date: Thu, 12 Sep 2024 23:53:21 +0800 Subject: [PATCH 048/159] Add option to always burn in subtitles if transcoding is triggered (#12430) --- .../Controllers/MediaInfoController.cs | 11 +++++++--- .../Controllers/UniversalAudioController.cs | 1 + Jellyfin.Api/Helpers/MediaInfoHelper.cs | 6 +++++- .../Models/MediaInfoDtos/OpenLiveStreamDto.cs | 5 +++++ .../Models/MediaInfoDtos/PlaybackInfoDto.cs | 5 +++++ MediaBrowser.Model/Dlna/MediaOptions.cs | 5 +++++ MediaBrowser.Model/Dlna/StreamBuilder.cs | 20 +++++++++++++++---- .../MediaInfo/LiveStreamRequest.cs | 3 +++ MediaBrowser.Model/Session/TranscodeReason.cs | 1 + 9 files changed, 49 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index bc52be1842..f22ac0b73a 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -209,6 +209,7 @@ public class MediaInfoController : BaseJellyfinApiController enableTranscoding.Value, allowVideoStreamCopy.Value, allowAudioStreamCopy.Value, + playbackInfoDto?.AlwaysBurnInSubtitleWhenTranscoding ?? false, Request.HttpContext.GetNormalizedRemoteIP()); } @@ -236,7 +237,8 @@ public class MediaInfoController : BaseJellyfinApiController StartTimeTicks = startTimeTicks, SubtitleStreamIndex = subtitleStreamIndex, UserId = userId ?? Guid.Empty, - OpenToken = mediaSource.OpenToken + OpenToken = mediaSource.OpenToken, + AlwaysBurnInSubtitleWhenTranscoding = playbackInfoDto?.AlwaysBurnInSubtitleWhenTranscoding ?? false }).ConfigureAwait(false); info.MediaSources = new[] { openStreamResult.MediaSource }; @@ -261,6 +263,7 @@ public class MediaInfoController : BaseJellyfinApiController /// The open live stream dto. /// Whether to enable direct play. Default: true. /// Whether to enable direct stream. Default: true. + /// Always burn-in subtitle when transcoding. /// Media source opened. /// A containing a . [HttpPost("LiveStreams/Open")] @@ -277,7 +280,8 @@ public class MediaInfoController : BaseJellyfinApiController [FromQuery] Guid? itemId, [FromBody] OpenLiveStreamDto? openLiveStreamDto, [FromQuery] bool? enableDirectPlay, - [FromQuery] bool? enableDirectStream) + [FromQuery] bool? enableDirectStream, + [FromQuery] bool? alwaysBurnInSubtitleWhenTranscoding) { userId ??= openLiveStreamDto?.UserId; userId = RequestHelpers.GetUserId(User, userId); @@ -295,7 +299,8 @@ public class MediaInfoController : BaseJellyfinApiController DeviceProfile = openLiveStreamDto?.DeviceProfile, EnableDirectPlay = enableDirectPlay ?? openLiveStreamDto?.EnableDirectPlay ?? true, EnableDirectStream = enableDirectStream ?? openLiveStreamDto?.EnableDirectStream ?? true, - DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http } + DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }, + AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding ?? openLiveStreamDto?.AlwaysBurnInSubtitleWhenTranscoding ?? false }; return await _mediaInfoHelper.OpenMediaSource(HttpContext, request).ConfigureAwait(false); } diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index fe73534967..41c4886d4f 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -160,6 +160,7 @@ public class UniversalAudioController : BaseJellyfinApiController true, true, true, + false, Request.HttpContext.GetNormalizedRemoteIP()); } diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 9bda27031b..5050cab418 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -156,6 +156,7 @@ public class MediaInfoHelper /// Enable transcoding. /// Allow video stream copy. /// Allow audio stream copy. + /// Always burn-in subtitle when transcoding. /// Requesting IP address. public void SetDeviceSpecificData( BaseItem item, @@ -175,6 +176,7 @@ public class MediaInfoHelper bool enableTranscoding, bool allowVideoStreamCopy, bool allowAudioStreamCopy, + bool alwaysBurnInSubtitleWhenTranscoding, IPAddress ipAddress) { var streamBuilder = new StreamBuilder(_mediaEncoder, _logger); @@ -188,7 +190,8 @@ public class MediaInfoHelper Profile = profile, MaxAudioChannels = maxAudioChannels, AllowAudioStreamCopy = allowAudioStreamCopy, - AllowVideoStreamCopy = allowVideoStreamCopy + AllowVideoStreamCopy = allowVideoStreamCopy, + AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding, }; if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase)) @@ -420,6 +423,7 @@ public class MediaInfoHelper true, true, true, + request.AlwaysBurnInSubtitleWhenTranscoding, httpContext.GetNormalizedRemoteIP()); } else diff --git a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs index 53104988f5..978e99b35c 100644 --- a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs +++ b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs @@ -65,6 +65,11 @@ public class OpenLiveStreamDto /// public bool? EnableDirectStream { get; set; } + /// + /// Gets or sets a value indicating whether always burn in subtitles when transcoding. + /// + public bool? AlwaysBurnInSubtitleWhenTranscoding { get; set; } + /// /// Gets or sets the device profile. /// diff --git a/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs index 9e12ddde65..82f603ca1e 100644 --- a/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs +++ b/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs @@ -82,4 +82,9 @@ public class PlaybackInfoDto /// Gets or sets a value indicating whether to auto open the live stream. /// public bool? AutoOpenLiveStream { get; set; } + + /// + /// Gets or sets a value indicating whether always burn in subtitles when transcoding. + /// + public bool? AlwaysBurnInSubtitleWhenTranscoding { get; set; } } diff --git a/MediaBrowser.Model/Dlna/MediaOptions.cs b/MediaBrowser.Model/Dlna/MediaOptions.cs index eca971e95e..6b26ca94b5 100644 --- a/MediaBrowser.Model/Dlna/MediaOptions.cs +++ b/MediaBrowser.Model/Dlna/MediaOptions.cs @@ -49,6 +49,11 @@ namespace MediaBrowser.Model.Dlna /// public bool AllowVideoStreamCopy { get; set; } + /// + /// Gets or sets a value indicating whether always burn in subtitles when transcoding. + /// + public bool AlwaysBurnInSubtitleWhenTranscoding { get; set; } + /// /// Gets or sets the item id. /// diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index f68a8bca34..e4492ac79b 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -20,8 +20,8 @@ namespace MediaBrowser.Model.Dlna // Aliases internal const TranscodeReason ContainerReasons = TranscodeReason.ContainerNotSupported | TranscodeReason.ContainerBitrateExceedsLimit; internal const TranscodeReason AudioReasons = TranscodeReason.AudioCodecNotSupported | TranscodeReason.AudioBitrateNotSupported | TranscodeReason.AudioChannelsNotSupported | TranscodeReason.AudioProfileNotSupported | TranscodeReason.AudioSampleRateNotSupported | TranscodeReason.SecondaryAudioNotSupported | TranscodeReason.AudioBitDepthNotSupported | TranscodeReason.AudioIsExternal; - internal const TranscodeReason VideoReasons = TranscodeReason.VideoCodecNotSupported | TranscodeReason.VideoResolutionNotSupported | TranscodeReason.AnamorphicVideoNotSupported | TranscodeReason.InterlacedVideoNotSupported | TranscodeReason.VideoBitDepthNotSupported | TranscodeReason.VideoBitrateNotSupported | TranscodeReason.VideoFramerateNotSupported | TranscodeReason.VideoLevelNotSupported | TranscodeReason.RefFramesNotSupported; - internal const TranscodeReason DirectStreamReasons = AudioReasons | TranscodeReason.ContainerNotSupported; + internal const TranscodeReason VideoReasons = TranscodeReason.VideoCodecNotSupported | TranscodeReason.VideoResolutionNotSupported | TranscodeReason.AnamorphicVideoNotSupported | TranscodeReason.InterlacedVideoNotSupported | TranscodeReason.VideoBitDepthNotSupported | TranscodeReason.VideoBitrateNotSupported | TranscodeReason.VideoFramerateNotSupported | TranscodeReason.VideoLevelNotSupported | TranscodeReason.RefFramesNotSupported | TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.VideoProfileNotSupported; + internal const TranscodeReason DirectStreamReasons = AudioReasons | TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecTagNotSupported; private readonly ILogger _logger; private readonly ITranscoderSupport _transcoderSupport; @@ -352,7 +352,7 @@ namespace MediaBrowser.Model.Dlna return TranscodeReason.VideoBitrateNotSupported; case ProfileConditionValue.VideoCodecTag: - return TranscodeReason.VideoCodecNotSupported; + return TranscodeReason.VideoCodecTagNotSupported; case ProfileConditionValue.VideoFramerate: return TranscodeReason.VideoFramerateNotSupported; @@ -765,7 +765,19 @@ namespace MediaBrowser.Model.Dlna { var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, _transcoderSupport, transcodingProfile.Container, transcodingProfile.Protocol); - playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method; + if (options.AlwaysBurnInSubtitleWhenTranscoding && (playlistItem.TranscodeReasons & (VideoReasons | TranscodeReason.ContainerBitrateExceedsLimit)) != 0) + { + playlistItem.SubtitleDeliveryMethod = SubtitleDeliveryMethod.Encode; + foreach (SubtitleProfile profile in options.Profile.SubtitleProfiles) + { + profile.Method = SubtitleDeliveryMethod.Encode; + } + } + else + { + playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method; + } + playlistItem.SubtitleFormat = subtitleProfile.Format; playlistItem.SubtitleCodecs = new[] { subtitleProfile.Format }; } diff --git a/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs index 24eab1a744..92f467eb08 100644 --- a/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs +++ b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs @@ -13,6 +13,7 @@ namespace MediaBrowser.Model.MediaInfo { EnableDirectPlay = true; EnableDirectStream = true; + AlwaysBurnInSubtitleWhenTranscoding = false; DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http }; } @@ -40,6 +41,8 @@ namespace MediaBrowser.Model.MediaInfo public bool EnableDirectStream { get; set; } + public bool AlwaysBurnInSubtitleWhenTranscoding { get; set; } + public IReadOnlyList DirectPlayProtocols { get; set; } } } diff --git a/MediaBrowser.Model/Session/TranscodeReason.cs b/MediaBrowser.Model/Session/TranscodeReason.cs index bbdf4536b7..39c5ac8fa4 100644 --- a/MediaBrowser.Model/Session/TranscodeReason.cs +++ b/MediaBrowser.Model/Session/TranscodeReason.cs @@ -18,6 +18,7 @@ namespace MediaBrowser.Model.Session // Video Constraints VideoProfileNotSupported = 1 << 6, VideoRangeTypeNotSupported = 1 << 24, + VideoCodecTagNotSupported = 1 << 25, VideoLevelNotSupported = 1 << 7, VideoResolutionNotSupported = 1 << 8, VideoBitDepthNotSupported = 1 << 9, From 6b646e24eabdfdd150f1ff72f837d37b43f3b985 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 12 Sep 2024 21:44:57 +0200 Subject: [PATCH 049/159] Don't extract chapter images if chapters are <1s long on average (#11832) --- .../MediaEncoder/EncodingManager.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 896f47923f..784bac5d04 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -91,8 +91,30 @@ namespace Emby.Server.Implementations.MediaEncoder return video.DefaultVideoStreamIndex.HasValue; } + private long GetAverageDurationBetweenChapters(IReadOnlyList chapters) + { + if (chapters.Count < 2) + { + return 0; + } + + long sum = 0; + for (int i = 1; i < chapters.Count; i++) + { + sum += chapters[i].StartPositionTicks - chapters[i - 1].StartPositionTicks; + } + + return sum / chapters.Count; + } + + public async Task RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken) { + if (chapters.Count == 0) + { + return true; + } + var libraryOptions = _libraryManager.GetLibraryOptions(video); if (!IsEligibleForChapterImageExtraction(video, libraryOptions)) @@ -100,6 +122,14 @@ namespace Emby.Server.Implementations.MediaEncoder extractImages = false; } + var averageChapterDuration = GetAverageDurationBetweenChapters(chapters); + var threshold = TimeSpan.FromSeconds(1).Ticks; + if (averageChapterDuration < threshold) + { + _logger.LogInformation("Skipping chapter image extraction for {Video} as the average chapter duration {AverageDuration} was lower than the minimum threshold {Threshold}", video.Name, averageChapterDuration, threshold); + extractImages = false; + } + var success = true; var changesMade = false; From 90a00e12937c5b9922af5024fd3e281f28edc17b Mon Sep 17 00:00:00 2001 From: gnattu Date: Fri, 13 Sep 2024 03:45:38 +0800 Subject: [PATCH 050/159] Only remove images in metadata folder by default (#12631) --- MediaBrowser.Providers/Manager/ItemImageProvider.cs | 10 ++++++++-- .../Manager/ItemImageProviderTests.cs | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 1bb7ffccec..36a7c2fabe 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -68,16 +68,22 @@ namespace MediaBrowser.Providers.Manager /// Removes all existing images from the provided item. /// /// The to remove images from. + /// Whether removing images outside metadata folder is allowed. /// true if changes were made to the item; otherwise false. - public bool RemoveImages(BaseItem item) + public bool RemoveImages(BaseItem item, bool canDeleteLocal = false) { var singular = new List(); + var itemMetadataPath = item.GetInternalMetadataPath(); for (var i = 0; i < _singularImages.Length; i++) { var currentImage = item.GetImageInfo(_singularImages[i], 0); if (currentImage is not null) { - singular.Add(currentImage); + var imageInMetadataFolder = currentImage.Path.StartsWith(itemMetadataPath, StringComparison.OrdinalIgnoreCase); + if (imageInMetadataFolder || canDeleteLocal || item.IsSaveLocalMetadataEnabled()) + { + singular.Add(currentImage); + } } } diff --git a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs index 5dd3eb8ab9..0c7d2487cb 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs @@ -580,6 +580,7 @@ namespace Jellyfin.Providers.Tests.Manager CallBase = true }; item.Setup(m => m.IsSaveLocalMetadataEnabled()).Returns(false); + item.Setup(m => m.GetInternalMetadataPath()).Returns(string.Empty); var path = validPaths ? _testDataImagePath.Format : "invalid path {0}"; for (int i = 0; i < count; i++) From ac55682260706f894536b09e5946e79cb14a9672 Mon Sep 17 00:00:00 2001 From: BromTeque Date: Thu, 12 Sep 2024 13:24:45 +0000 Subject: [PATCH 051/159] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)=20Translation:=20Jellyfin/Jellyfin=20Translat?= =?UTF-8?q?e-URL:=20https://translate.jellyfin.org/projects/jellyfin/jelly?= =?UTF-8?q?fin-core/nb=5FNO/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Emby.Server.Implementations/Localization/Core/nb.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index 7476525387..6d644976d2 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -132,5 +132,6 @@ "TaskAudioNormalizationDescription": "Skan filer for lyd normaliserende data", "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra kolleksjoner og spillelister som ikke lengere finnes", "TaskDownloadMissingLyrics": "Last ned manglende tekster", - "TaskDownloadMissingLyricsDescription": "Last ned sangtekster" + "TaskDownloadMissingLyricsDescription": "Last ned sangtekster", + "TaskExtractMediaSegments": "Skann mediasegment" } From acbe4082a8356806dc7ce8d45e6be40908b822e7 Mon Sep 17 00:00:00 2001 From: Ruben Teixeira Date: Thu, 12 Sep 2024 19:24:10 +0000 Subject: [PATCH 052/159] Translated using Weblate (Portuguese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/ --- Emby.Server.Implementations/Localization/Core/pt.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index d157547def..7e9be76e51 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -131,5 +131,9 @@ "TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.", "TaskAudioNormalization": "Normalização de áudio", "TaskDownloadMissingLyrics": "Baixar letras faltantes", - "TaskDownloadMissingLyricsDescription": "Baixa letras para músicas" + "TaskDownloadMissingLyricsDescription": "Baixa letras para músicas", + "TaskMoveTrickplayImagesDescription": "Transfere ficheiros de miniatura de vídeo, conforme as definições da biblioteca.", + "TaskExtractMediaSegments": "Varrimento de segmentos da média", + "TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de média de extensões com suporte a MediaSegment.", + "TaskMoveTrickplayImages": "Migração de miniaturas de vídeo" } From a529edaad17fa048d654fcd90dd71a7454fe3174 Mon Sep 17 00:00:00 2001 From: Josh Hood Date: Wed, 11 Sep 2024 21:47:22 +0000 Subject: [PATCH 053/159] Translated using Weblate (Cornish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/kw/ --- Emby.Server.Implementations/Localization/Core/kw.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/kw.json b/Emby.Server.Implementations/Localization/Core/kw.json index ffb4345c89..336d286fc4 100644 --- a/Emby.Server.Implementations/Localization/Core/kw.json +++ b/Emby.Server.Implementations/Localization/Core/kw.json @@ -131,5 +131,9 @@ "TaskCleanCollectionsAndPlaylists": "Glanhe kuntellow ha rolyow-gwari", "TaskKeyframeExtractor": "Estennell Framalhwedh", "TaskCleanCollectionsAndPlaylistsDescription": "Y hwra dilea taklow a-dhyworth kuntellow ha rolyow-gwari na vos na moy.", - "TaskKeyframeExtractorDescription": "Y hwra kuntel framyowalhwedh a-dhyworth restrennow gwydhyowyow rag gul rolyow-gwari HLS moy poran. Martesen y hwra an oberen ma ow ponya rag termyn hir." + "TaskKeyframeExtractorDescription": "Y hwra kuntel framyowalhwedh a-dhyworth restrennow gwydhyowyow rag gul rolyow-gwari HLS moy poran. Martesen y hwra an oberen ma ow ponya rag termyn hir.", + "TaskExtractMediaSegments": "Arhwilas Rann Media", + "TaskExtractMediaSegmentsDescription": "Kavos rannow media a-dhyworth ystynansow gallosegys MediaSegment.", + "TaskMoveTrickplayImages": "Divroa Tyller Imach TrickPlay", + "TaskMoveTrickplayImagesDescription": "Y hwra movya restrennow a-lemmyn trickplay herwydh settyansow lyverva." } From 3c64e1d33fecc4815a9f491d08833310e7a906a2 Mon Sep 17 00:00:00 2001 From: gnattu Date: Fri, 13 Sep 2024 10:07:10 +0800 Subject: [PATCH 054/159] Remove redundant newline to fix CI Signed-off-by: gnattu --- Emby.Server.Implementations/MediaEncoder/EncodingManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 784bac5d04..eb55e32c50 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -107,7 +107,6 @@ namespace Emby.Server.Implementations.MediaEncoder return sum / chapters.Count; } - public async Task RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken) { if (chapters.Count == 0) From 6395f4889d18bf4b12567ca7c28e9d5a22506e73 Mon Sep 17 00:00:00 2001 From: gnattu Date: Fri, 13 Sep 2024 15:44:03 +0800 Subject: [PATCH 055/159] Update unit test for StreamBuilder to reflect current server and clients Signed-off-by: gnattu --- .../Dlna/StreamBuilderTests.cs | 192 +++--- .../Test Data/DeviceProfile-Chrome.json | 393 +++++------- .../Test Data/DeviceProfile-Firefox.json | 563 ++++++++---------- .../Test Data/DeviceProfile-SafariNext.json | 322 +++++----- ...iaSourceInfo-mp4-h264-hi10p-aac-5000k.json | 86 +++ 5 files changed, 739 insertions(+), 817 deletions(-) create mode 100644 tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-hi10p-aac-5000k.json diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 3429d1a5bd..9953431d92 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -22,37 +22,41 @@ namespace Jellyfin.Model.Tests [Theory] // Chrome [InlineData("Chrome", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 - [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 - [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 - [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] - [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] - [InlineData("Chrome", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")] - [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported, "Remux")] // #6450 - [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioIsExternal, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)] + [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] + [InlineData("Chrome", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "HLS.mp4")] + [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome", "mp4-h264-hi10p-aac-5000k", PlayMethod.DirectPlay)] // Firefox [InlineData("Firefox", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 - [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 - [InlineData("Firefox", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 - [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] - [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] - [InlineData("Firefox", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")] - [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported, "Remux")] // #6450 - [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioIsExternal, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "HLS.mp4")] + [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")] + [InlineData("Firefox", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode", "HLS.mp4")] + [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Firefox", "mp4-h264-hi10p-aac-5000k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")] // TODO: investigate why firefox profile has everything unsupported // Safari [InlineData("SafariNext", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("SafariNext", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("SafariNext", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 - [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 - [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 - [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mp4-h264-hi10p-aac-5000k", PlayMethod.Transcode, TranscodeReason.VideoProfileNotSupported, "Remux", "HLS.mp4")] + // AndroidPixel [InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("AndroidPixel", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 @@ -62,21 +66,21 @@ namespace Jellyfin.Model.Tests [InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // Yatse [InlineData("Yatse", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 - [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 - [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] + [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] [InlineData("Yatse", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 - [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Yatse", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 + [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc + [InlineData("Yatse", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc // RokuSSPlus [InlineData("RokuSSPlus", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 - [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("RokuSSPlus", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 should be DirectPlay - [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("RokuSSPlus", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 - [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc [InlineData("RokuSSPlus", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 - [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc // JellyfinMediaPlayer [InlineData("JellyfinMediaPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 @@ -86,18 +90,6 @@ namespace Jellyfin.Model.Tests [InlineData("JellyfinMediaPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("JellyfinMediaPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("JellyfinMediaPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450 - // Chrome-NoHLS - [InlineData("Chrome-NoHLS", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 - [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 - [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 - [InlineData("Chrome-NoHLS", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Chrome-NoHLS", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")] - [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "http")] - [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode", "http")] - [InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported, "Remux")] // #6450 - [InlineData("Chrome-NoHLS", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Chrome-NoHLS", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 // TranscodeMedia [InlineData("TranscodeMedia", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")] [InlineData("TranscodeMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "HLS.mp4")] @@ -147,7 +139,7 @@ namespace Jellyfin.Model.Tests [InlineData("AndroidTVExoPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)] [InlineData("AndroidTVExoPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("AndroidTVExoPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] - [InlineData("AndroidTVExoPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] + [InlineData("AndroidTVExoPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow vp9 // Tizen 3 Stereo [InlineData("Tizen3-stereo", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] @@ -155,7 +147,7 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen3-stereo", "mp4-h264-dts-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen3-stereo", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)] [InlineData("Tizen3-stereo", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)] - [InlineData("Tizen3-stereo", "mp4-hevc-truehd-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] + [InlineData("Tizen3-stereo", "mp4-hevc-truehd-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] [InlineData("Tizen3-stereo", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen3-stereo", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen3-stereo", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] @@ -163,10 +155,10 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] - [InlineData("Tizen4-4K-5.1", "mp4-h264-dts-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] + [InlineData("Tizen4-4K-5.1", "mp4-h264-dts-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] [InlineData("Tizen4-4K-5.1", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)] - [InlineData("Tizen4-4K-5.1", "mp4-hevc-truehd-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] + [InlineData("Tizen4-4K-5.1", "mp4-hevc-truehd-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] @@ -179,24 +171,24 @@ namespace Jellyfin.Model.Tests [Theory] // Chrome [InlineData("Chrome", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 - [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] - [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] - [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported, "Remux")] // #6450 - [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)] + [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] + [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux", "HLS.mp4")] // #6450 // Firefox [InlineData("Firefox", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 - [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] - [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] - [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported, "Remux")] // #6450 - [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "HLS.mp4")] + [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")] + [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 // Safari [InlineData("SafariNext", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 @@ -204,9 +196,10 @@ namespace Jellyfin.Model.Tests [InlineData("SafariNext", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 - [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 - [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 - [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 + // AndroidPixel [InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("AndroidPixel", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 @@ -215,19 +208,19 @@ namespace Jellyfin.Model.Tests [InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // Yatse [InlineData("Yatse", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 - [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] + [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] [InlineData("Yatse", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 - [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc // RokuSSPlus [InlineData("RokuSSPlus", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 - [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay - [InlineData("RokuSSPlus", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("RokuSSPlus", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("RokuSSPlus", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 - [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 - [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc + [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc // JellyfinMediaPlayer [InlineData("JellyfinMediaPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 @@ -245,7 +238,7 @@ namespace Jellyfin.Model.Tests [InlineData("AndroidTVExoPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)] [InlineData("AndroidTVExoPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("AndroidTVExoPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] - [InlineData("AndroidTVExoPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] + [InlineData("AndroidTVExoPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow vp9 // Tizen 3 Stereo [InlineData("Tizen3-stereo", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] @@ -253,7 +246,7 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen3-stereo", "mp4-h264-dts-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen3-stereo", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)] [InlineData("Tizen3-stereo", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)] - [InlineData("Tizen3-stereo", "mp4-hevc-truehd-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] + [InlineData("Tizen3-stereo", "mp4-hevc-truehd-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] [InlineData("Tizen3-stereo", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen3-stereo", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen3-stereo", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] @@ -261,10 +254,10 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] - [InlineData("Tizen4-4K-5.1", "mp4-h264-dts-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] + [InlineData("Tizen4-4K-5.1", "mp4-h264-dts-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] [InlineData("Tizen4-4K-5.1", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)] - [InlineData("Tizen4-4K-5.1", "mp4-hevc-truehd-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] + [InlineData("Tizen4-4K-5.1", "mp4-hevc-truehd-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] @@ -281,34 +274,34 @@ namespace Jellyfin.Model.Tests [Theory] // Chrome - [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 - [InlineData("Chrome", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] - [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 - [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")] + [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "HLS.mp4")] + [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioIsExternal, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "HLS.mp4")] // Firefox - [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 - [InlineData("Firefox", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] - [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")] + [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "HLS.mp4")] + [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode", "HLS.mp4")] // Yatse - [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 - [InlineData("Yatse", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] - [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")] + [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc // RokuSSPlus [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 // no streams - [InlineData("Chrome", "no-streams", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] // #6450 + [InlineData("Chrome", "no-streams", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "HLS.mp4")] // #6450 // AndroidTV [InlineData("AndroidTVExoPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] [InlineData("AndroidTVExoPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // Tizen 3 Stereo - [InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] - [InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] - [InlineData("Tizen3-stereo", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] + [InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")] + [InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")] + [InlineData("Tizen3-stereo", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // Tizen 4 4K 5.1 - [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] - [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] - [InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] + [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")] + [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")] + [InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // TranscodeMedia [InlineData("TranscodeMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")] [InlineData("TranscodeMedia", "mp4-h264-ac3-aac-mp3-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.ts")] @@ -331,7 +324,7 @@ namespace Jellyfin.Model.Tests { if (string.IsNullOrEmpty(transcodeProtocol)) { - transcodeProtocol = playMethod == PlayMethod.DirectStream ? "http" : "HLS.ts"; + transcodeProtocol = "HLS.ts"; } var builder = GetStreamBuilder(); @@ -380,7 +373,7 @@ namespace Jellyfin.Model.Tests Assert.Equal(streamInfo.Container, uri.Extension); } } - else if (playMethod == PlayMethod.DirectStream || playMethod == PlayMethod.Transcode) + else if (playMethod == PlayMethod.Transcode) { Assert.NotNull(streamInfo.Container); Assert.NotEmpty(streamInfo.VideoCodecs); @@ -550,6 +543,7 @@ namespace Jellyfin.Model.Tests Profile = dp, AllowAudioStreamCopy = true, AllowVideoStreamCopy = true, + EnableDirectStream = false // This is disabled in server }; } diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json index 81bb97ac82..e2f75b569b 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json @@ -16,324 +16,200 @@ "DirectPlayProfiles": [ { "Container": "webm", - "AudioCodec": "vorbis,opus", - "VideoCodec": "vp8,vp9,av1", "Type": "Video", - "$type": "DirectPlayProfile" + "VideoCodec": "vp8,vp9,av1", + "AudioCodec": "vorbis,opus" }, { "Container": "mp4,m4v", - "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", - "VideoCodec": "h264,vp8,vp9,av1", "Type": "Video", - "$type": "DirectPlayProfile" + "VideoCodec": "h264,hevc,vp9,av1", + "AudioCodec": "aac,mp3,mp2,opus,flac,vorbis" }, { "Container": "mov", - "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", - "VideoCodec": "h264", "Type": "Video", - "$type": "DirectPlayProfile" + "VideoCodec": "h264", + "AudioCodec": "aac,mp3,mp2,opus,flac,vorbis" }, { "Container": "opus", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "webm", "AudioCodec": "opus", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" + }, + { + "Container": "ts", + "AudioCodec": "mp3", + "Type": "Audio" }, { "Container": "mp3", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "aac", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "m4a", "AudioCodec": "aac", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "m4b", "AudioCodec": "aac", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "flac", - "Type": "Audio", - "$type": "DirectPlayProfile" - }, - { - "Container": "alac", - "Type": "Audio", - "$type": "DirectPlayProfile" - }, - { - "Container": "m4a", - "AudioCodec": "alac", - "Type": "Audio", - "$type": "DirectPlayProfile" - }, - { - "Container": "m4b", - "AudioCodec": "alac", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "webma", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "webm", "AudioCodec": "webma", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "wav", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "ogg", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" + }, + { + "Container": "hls", + "Type": "Video", + "VideoCodec": "av1,hevc,h264,vp9", + "AudioCodec": "aac,mp2,opus,flac" + }, + { + "Container": "hls", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac,mp3,mp2" } ], "TranscodingProfiles": [ { - "Container": "ts", + "Container": "mp4", "Type": "Audio", "AudioCodec": "aac", - "Protocol": "hls", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, "Context": "Streaming", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 2, - "SegmentLength": 0, + "Protocol": "hls", + "MaxAudioChannels": "2", + "MinSegments": "2", "BreakOnNonKeyFrames": true, - "$type": "TranscodingProfile" + "EnableAudioVbrEncoding": true }, { "Container": "aac", "Type": "Audio", "AudioCodec": "aac", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, "Context": "Streaming", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" + "Protocol": "http", + "MaxAudioChannels": "2" }, { "Container": "mp3", "Type": "Audio", "AudioCodec": "mp3", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, "Context": "Streaming", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" + "Protocol": "http", + "MaxAudioChannels": "2" }, { "Container": "opus", "Type": "Audio", "AudioCodec": "opus", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, "Context": "Streaming", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" + "Protocol": "http", + "MaxAudioChannels": "2" }, { "Container": "wav", "Type": "Audio", "AudioCodec": "wav", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, "Context": "Streaming", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" + "Protocol": "http", + "MaxAudioChannels": "2" }, { "Container": "opus", "Type": "Audio", "AudioCodec": "opus", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, "Context": "Static", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" + "Protocol": "http", + "MaxAudioChannels": "2" }, { "Container": "mp3", "Type": "Audio", "AudioCodec": "mp3", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, "Context": "Static", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" + "Protocol": "http", + "MaxAudioChannels": "2" }, { "Container": "aac", "Type": "Audio", "AudioCodec": "aac", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, "Context": "Static", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" + "Protocol": "http", + "MaxAudioChannels": "2" }, { "Container": "wav", "Type": "Audio", "AudioCodec": "wav", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, "Context": "Static", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" - }, - { - "Container": "ts", - "Type": "Video", - "VideoCodec": "h264", - "AudioCodec": "aac,mp3", - "Protocol": "hls", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, - "Context": "Streaming", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 2, - "SegmentLength": 0, - "BreakOnNonKeyFrames": true, - "$type": "TranscodingProfile" - }, - { - "Container": "webm", - "Type": "Video", - "VideoCodec": "vp8,vp9,av1,vpx", - "AudioCodec": "vorbis,opus", "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, - "Context": "Streaming", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" + "MaxAudioChannels": "2" }, { "Container": "mp4", "Type": "Video", + "AudioCodec": "aac,mp2,opus,flac", + "VideoCodec": "av1,hevc,h264,vp9", + "Context": "Streaming", + "Protocol": "hls", + "MaxAudioChannels": "2", + "MinSegments": "2", + "BreakOnNonKeyFrames": true + }, + { + "Container": "ts", + "Type": "Video", + "AudioCodec": "aac,mp3,mp2", "VideoCodec": "h264", - "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, - "Context": "Static", - "EnableSubtitlesInManifest": false, - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" + "Context": "Streaming", + "Protocol": "hls", + "MaxAudioChannels": "2", + "MinSegments": "2", + "BreakOnNonKeyFrames": true } ], + "ContainerProfiles": [], "CodecProfiles": [ { "Type": "VideoAudio", + "Codec": "aac", "Conditions": [ { "Condition": "Equals", "Property": "IsSecondaryAudio", "Value": "false", - "IsRequired": false, - "$type": "ProfileCondition" + "IsRequired": false } - ], - "Codec": "aac", - "$type": "CodecProfile" + ] }, { "Type": "VideoAudio", @@ -342,107 +218,144 @@ "Condition": "Equals", "Property": "IsSecondaryAudio", "Value": "false", - "IsRequired": false, - "$type": "ProfileCondition" + "IsRequired": false } - ], - "$type": "CodecProfile" + ] }, { "Type": "Video", + "Codec": "h264", "Conditions": [ { "Condition": "NotEquals", "Property": "IsAnamorphic", "Value": "true", - "IsRequired": false, - "$type": "ProfileCondition" + "IsRequired": false }, { "Condition": "EqualsAny", "Property": "VideoProfile", "Value": "high|main|baseline|constrained baseline|high 10", - "IsRequired": false, - "$type": "ProfileCondition" + "IsRequired": false + }, + { + "Condition": "EqualsAny", + "Property": "VideoRangeType", + "Value": "SDR", + "IsRequired": false }, { "Condition": "LessThanEqual", "Property": "VideoLevel", "Value": "52", - "IsRequired": false, - "$type": "ProfileCondition" + "IsRequired": false }, { "Condition": "NotEquals", "Property": "IsInterlaced", "Value": "true", - "IsRequired": false, - "$type": "ProfileCondition" + "IsRequired": false } - ], - "Codec": "h264", - "$type": "CodecProfile" + ] }, { "Type": "Video", + "Codec": "hevc", "Conditions": [ { "Condition": "NotEquals", "Property": "IsAnamorphic", "Value": "true", - "IsRequired": false, - "$type": "ProfileCondition" + "IsRequired": false }, { "Condition": "EqualsAny", "Property": "VideoProfile", - "Value": "main", - "IsRequired": false, - "$type": "ProfileCondition" + "Value": "main|main 10", + "IsRequired": false + }, + { + "Condition": "EqualsAny", + "Property": "VideoRangeType", + "Value": "SDR|HDR10|HLG", + "IsRequired": false }, { "Condition": "LessThanEqual", "Property": "VideoLevel", - "Value": "120", - "IsRequired": false, - "$type": "ProfileCondition" + "Value": "183", + "IsRequired": false }, { "Condition": "NotEquals", "Property": "IsInterlaced", "Value": "true", - "IsRequired": false, - "$type": "ProfileCondition" + "IsRequired": false } - ], - "Codec": "hevc", - "$type": "CodecProfile" - } - ], - "ResponseProfiles": [ + ] + }, { - "Container": "m4v", "Type": "Video", - "MimeType": "video/mp4", - "$type": "ResponseProfile" + "Codec": "vp9", + "Conditions": [ + { + "Condition": "EqualsAny", + "Property": "VideoRangeType", + "Value": "SDR|HDR10|HLG", + "IsRequired": false + } + ] + }, + { + "Type": "Video", + "Codec": "av1", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "main", + "IsRequired": false + }, + { + "Condition": "EqualsAny", + "Property": "VideoRangeType", + "Value": "SDR|HDR10|HLG", + "IsRequired": false + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "19", + "IsRequired": false + } + ] } ], "SubtitleProfiles": [ { "Format": "vtt", - "Method": "External", - "$type": "SubtitleProfile" + "Method": "External" }, { "Format": "ass", - "Method": "External", - "$type": "SubtitleProfile" + "Method": "External" }, { "Format": "ssa", - "Method": "External", - "$type": "SubtitleProfile" + "Method": "External" } ], - "$type": "DeviceProfile" + "ResponseProfiles": [ + { + "Type": "Video", + "Container": "m4v", + "MimeType": "video/mp4" + } + ] } diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json index 9874793d37..21ae7e5cb3 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json @@ -15,426 +15,357 @@ "IgnoreTranscodeByteRangeRequests": false, "DirectPlayProfiles": [ { - "Container": "webm", "AudioCodec": "vorbis,opus", - "VideoCodec": "vp8,vp9,av1", + "Container": "webm", "Type": "Video", - "$type": "DirectPlayProfile" + "VideoCodec": "vp8,vp9,av1" }, { + "AudioCodec": "aac,mp3,mp2,opus,flac,vorbis", "Container": "mp4,m4v", - "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", - "VideoCodec": "h264,vp8,vp9,av1", "Type": "Video", - "$type": "DirectPlayProfile" + "VideoCodec": "h264,vp9,av1" }, { "Container": "opus", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { - "Container": "webm", "AudioCodec": "opus", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Container": "webm", + "Type": "Audio" + }, + { + "AudioCodec": "mp3", + "Container": "ts", + "Type": "Audio" }, { "Container": "mp3", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "aac", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { + "AudioCodec": "aac", "Container": "m4a", - "AudioCodec": "aac", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { - "Container": "m4b", "AudioCodec": "aac", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Container": "m4b", + "Type": "Audio" }, { "Container": "flac", - "Type": "Audio", - "$type": "DirectPlayProfile" - }, - { - "Container": "alac", - "Type": "Audio", - "$type": "DirectPlayProfile" - }, - { - "Container": "m4a", - "AudioCodec": "alac", - "Type": "Audio", - "$type": "DirectPlayProfile" - }, - { - "Container": "m4b", - "AudioCodec": "alac", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "webma", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { - "Container": "webm", "AudioCodec": "webma", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Container": "webm", + "Type": "Audio" }, { "Container": "wav", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "ogg", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" + }, + { + "AudioCodec": "aac,mp2,opus,flac", + "Container": "hls", + "Type": "Video", + "VideoCodec": "av1,h264,vp9" + }, + { + "AudioCodec": "aac,mp3,mp2", + "Container": "hls", + "Type": "Video", + "VideoCodec": "h264" } ], "TranscodingProfiles": [ { - "Container": "ts", - "Type": "Audio", "AudioCodec": "aac", - "Protocol": "hls", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, - "Context": "Streaming", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 2, - "SegmentLength": 0, "BreakOnNonKeyFrames": true, - "$type": "TranscodingProfile" - }, - { - "Container": "aac", - "Type": "Audio", - "AudioCodec": "aac", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, - "Context": "Streaming", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" - }, - { - "Container": "mp3", - "Type": "Audio", - "AudioCodec": "mp3", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, - "Context": "Streaming", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" - }, - { - "Container": "opus", - "Type": "Audio", - "AudioCodec": "opus", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, - "Context": "Streaming", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" - }, - { - "Container": "wav", - "Type": "Audio", - "AudioCodec": "wav", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, - "Context": "Streaming", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" - }, - { - "Container": "opus", - "Type": "Audio", - "AudioCodec": "opus", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, - "Context": "Static", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" - }, - { - "Container": "mp3", - "Type": "Audio", - "AudioCodec": "mp3", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, - "Context": "Static", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" - }, - { - "Container": "aac", - "Type": "Audio", - "AudioCodec": "aac", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, - "Context": "Static", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" - }, - { - "Container": "wav", - "Type": "Audio", - "AudioCodec": "wav", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, - "Context": "Static", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" - }, - { - "Container": "ts", - "Type": "Video", - "VideoCodec": "h264", - "AudioCodec": "aac,mp3", - "Protocol": "hls", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, - "Context": "Streaming", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 2, - "SegmentLength": 0, - "BreakOnNonKeyFrames": true, - "$type": "TranscodingProfile" - }, - { - "Container": "webm", - "Type": "Video", - "VideoCodec": "vp8,vp9,av1,vpx", - "AudioCodec": "vorbis,opus", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, - "Context": "Streaming", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" - }, - { "Container": "mp4", - "Type": "Video", - "VideoCodec": "h264", - "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", + "Context": "Streaming", + "EnableAudioVbrEncoding": true, + "MaxAudioChannels": "2", + "MinSegments": "2", + "Protocol": "hls", + "Type": "Audio" + }, + { + "AudioCodec": "aac", + "Container": "aac", + "Context": "Streaming", + "MaxAudioChannels": "2", "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, + "Type": "Audio" + }, + { + "AudioCodec": "mp3", + "Container": "mp3", + "Context": "Streaming", + "MaxAudioChannels": "2", + "Protocol": "http", + "Type": "Audio" + }, + { + "AudioCodec": "opus", + "Container": "opus", + "Context": "Streaming", + "MaxAudioChannels": "2", + "Protocol": "http", + "Type": "Audio" + }, + { + "AudioCodec": "wav", + "Container": "wav", + "Context": "Streaming", + "MaxAudioChannels": "2", + "Protocol": "http", + "Type": "Audio" + }, + { + "AudioCodec": "opus", + "Container": "opus", "Context": "Static", - "EnableSubtitlesInManifest": false, - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" + "MaxAudioChannels": "2", + "Protocol": "http", + "Type": "Audio" + }, + { + "AudioCodec": "mp3", + "Container": "mp3", + "Context": "Static", + "MaxAudioChannels": "2", + "Protocol": "http", + "Type": "Audio" + }, + { + "AudioCodec": "aac", + "Container": "aac", + "Context": "Static", + "MaxAudioChannels": "2", + "Protocol": "http", + "Type": "Audio" + }, + { + "AudioCodec": "wav", + "Container": "wav", + "Context": "Static", + "MaxAudioChannels": "2", + "Protocol": "http", + "Type": "Audio" + }, + { + "AudioCodec": "aac,mp2,opus,flac", + "BreakOnNonKeyFrames": true, + "Container": "mp4", + "Context": "Streaming", + "MaxAudioChannels": "2", + "MinSegments": "2", + "Protocol": "hls", + "Type": "Video", + "VideoCodec": "av1,h264,vp9" + }, + { + "AudioCodec": "aac,mp3,mp2", + "BreakOnNonKeyFrames": true, + "Container": "ts", + "Context": "Streaming", + "MaxAudioChannels": "2", + "MinSegments": "2", + "Protocol": "hls", + "Type": "Video", + "VideoCodec": "h264" } ], "CodecProfiles": [ { - "Type": "VideoAudio", - "Conditions": [ - { - "Condition": "Equals", - "Property": "IsSecondaryAudio", - "Value": "false", - "IsRequired": false, - "$type": "ProfileCondition" - } - ], "Codec": "aac", - "$type": "CodecProfile" - }, - { - "Type": "VideoAudio", "Conditions": [ { "Condition": "Equals", + "IsRequired": false, "Property": "IsSecondaryAudio", - "Value": "false", - "IsRequired": false, - "$type": "ProfileCondition" + "Value": "false" } ], - "$type": "CodecProfile" + "Type": "VideoAudio" }, { - "Type": "Video", "Conditions": [ - { - "Condition": "NotEquals", - "Property": "IsAnamorphic", - "Value": "true", - "IsRequired": false, - "$type": "ProfileCondition" - }, - { - "Condition": "EqualsAny", - "Property": "VideoProfile", - "Value": "high|main|baseline|constrained baseline", - "IsRequired": false, - "$type": "ProfileCondition" - }, { "Condition": "LessThanEqual", - "Property": "VideoLevel", - "Value": "52", "IsRequired": false, - "$type": "ProfileCondition" - }, - { - "Condition": "NotEquals", - "Property": "IsInterlaced", - "Value": "true", - "IsRequired": false, - "$type": "ProfileCondition" + "Property": "AudioChannels", + "Value": "2" } ], + "Type": "Audio" + }, + { + "Conditions": [ + { + "Condition": "LessThanEqual", + "IsRequired": false, + "Property": "AudioChannels", + "Value": "2" + }, + { + "Condition": "Equals", + "IsRequired": false, + "Property": "IsSecondaryAudio", + "Value": "false" + } + ], + "Type": "VideoAudio" + }, + { "Codec": "h264", - "$type": "CodecProfile" - }, - { - "Type": "Video", "Conditions": [ { "Condition": "NotEquals", - "Property": "IsAnamorphic", - "Value": "true", "IsRequired": false, - "$type": "ProfileCondition" + "Property": "IsAnamorphic", + "Value": "true" }, { "Condition": "EqualsAny", - "Property": "VideoProfile", - "Value": "main", "IsRequired": false, - "$type": "ProfileCondition" + "Property": "VideoProfile", + "Value": "high|main|baseline|constrained baseline" + }, + { + "Condition": "EqualsAny", + "IsRequired": false, + "Property": "VideoRangeType", + "Value": "SDR" }, { "Condition": "LessThanEqual", - "Property": "VideoLevel", - "Value": "120", "IsRequired": false, - "$type": "ProfileCondition" + "Property": "VideoLevel", + "Value": "52" }, { "Condition": "NotEquals", - "Property": "IsInterlaced", - "Value": "true", "IsRequired": false, - "$type": "ProfileCondition" + "Property": "IsInterlaced", + "Value": "true" } ], + "Type": "Video" + }, + { "Codec": "hevc", - "$type": "CodecProfile" + "Conditions": [ + { + "Condition": "NotEquals", + "IsRequired": false, + "Property": "IsAnamorphic", + "Value": "true" + }, + { + "Condition": "EqualsAny", + "IsRequired": false, + "Property": "VideoProfile", + "Value": "main" + }, + { + "Condition": "EqualsAny", + "IsRequired": false, + "Property": "VideoRangeType", + "Value": "SDR" + }, + { + "Condition": "LessThanEqual", + "IsRequired": false, + "Property": "VideoLevel", + "Value": "120" + }, + { + "Condition": "NotEquals", + "IsRequired": false, + "Property": "IsInterlaced", + "Value": "true" + } + ], + "Type": "Video" + }, + { + "Codec": "vp9", + "Conditions": [ + { + "Condition": "EqualsAny", + "IsRequired": false, + "Property": "VideoRangeType", + "Value": "SDR" + } + ], + "Type": "Video" + }, + { + "Codec": "av1", + "Conditions": [ + { + "Condition": "NotEquals", + "IsRequired": false, + "Property": "IsAnamorphic", + "Value": "true" + }, + { + "Condition": "EqualsAny", + "IsRequired": false, + "Property": "VideoProfile", + "Value": "main" + }, + { + "Condition": "EqualsAny", + "IsRequired": false, + "Property": "VideoRangeType", + "Value": "SDR" + }, + { + "Condition": "LessThanEqual", + "IsRequired": false, + "Property": "VideoLevel", + "Value": "19" + } + ], + "Type": "Video" } ], "ResponseProfiles": [ { "Container": "m4v", - "Type": "Video", "MimeType": "video/mp4", - "$type": "ResponseProfile" + "Type": "Video" } ], "SubtitleProfiles": [ { "Format": "vtt", - "Method": "External", - "$type": "SubtitleProfile" + "Method": "External" }, { "Format": "ass", - "Method": "External", - "$type": "SubtitleProfile" + "Method": "External" }, { "Format": "ssa", - "Method": "External", - "$type": "SubtitleProfile" + "Method": "External" } ], "$type": "DeviceProfile" diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json index 3b5a0c2549..f61d0e36bd 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json @@ -16,211 +16,160 @@ "DirectPlayProfiles": [ { "Container": "webm", - "AudioCodec": "vorbis", - "VideoCodec": "vp8,vp9", "Type": "Video", - "$type": "DirectPlayProfile" + "VideoCodec": "vp8", + "AudioCodec": "vorbis,opus" }, { "Container": "mp4,m4v", - "AudioCodec": "aac,mp3,ac3,eac3,flac,alac,vorbis", - "VideoCodec": "h264,vp8,vp9", "Type": "Video", - "$type": "DirectPlayProfile" + "VideoCodec": "h264,hevc,vp9", + "AudioCodec": "aac,ac3,eac3,opus,flac,alac" }, { "Container": "mov", - "AudioCodec": "aac,mp3,ac3,eac3,flac,alac,vorbis", - "VideoCodec": "h264", "Type": "Video", - "$type": "DirectPlayProfile" + "VideoCodec": "h264", + "AudioCodec": "aac,ac3,eac3,opus,flac,alac" + }, + { + "Container": "ts", + "AudioCodec": "mp3", + "Type": "Audio" }, { "Container": "mp3", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "aac", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "m4a", "AudioCodec": "aac", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "m4b", "AudioCodec": "aac", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { - "Container": "flac", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Container": "mp4", + "AudioCodec": "flac", + "Type": "Audio" }, { "Container": "alac", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "m4a", "AudioCodec": "alac", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "m4b", "AudioCodec": "alac", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "webma", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "webm", "AudioCodec": "webma", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" }, { "Container": "wav", - "Type": "Audio", - "$type": "DirectPlayProfile" + "Type": "Audio" + }, + { + "Container": "mp4", + "AudioCodec": "opus", + "Type": "Audio" + }, + { + "Container": "hls", + "Type": "Video", + "VideoCodec": "hevc,h264,vp9", + "AudioCodec": "aac,ac3,eac3,opus,flac,alac" + }, + { + "Container": "hls", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac,mp3,ac3,eac3" } ], "TranscodingProfiles": [ { - "Container": "aac", + "Container": "mp4", "Type": "Audio", "AudioCodec": "aac", + "Context": "Streaming", "Protocol": "hls", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, - "Context": "Streaming", - "EnableSubtitlesInManifest": false, "MaxAudioChannels": "6", - "MinSegments": 2, - "SegmentLength": 0, + "MinSegments": "2", "BreakOnNonKeyFrames": true, - "$type": "TranscodingProfile" + "EnableAudioVbrEncoding": true }, { "Container": "aac", "Type": "Audio", "AudioCodec": "aac", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, "Context": "Streaming", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" + "Protocol": "http", + "MaxAudioChannels": "6" }, { "Container": "mp3", "Type": "Audio", "AudioCodec": "mp3", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, "Context": "Streaming", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" + "Protocol": "http", + "MaxAudioChannels": "6" }, { "Container": "wav", "Type": "Audio", "AudioCodec": "wav", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, "Context": "Streaming", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" + "Protocol": "http", + "MaxAudioChannels": "6" }, { "Container": "mp3", "Type": "Audio", "AudioCodec": "mp3", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, "Context": "Static", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" + "Protocol": "http", + "MaxAudioChannels": "6" }, { "Container": "aac", "Type": "Audio", "AudioCodec": "aac", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, "Context": "Static", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" + "Protocol": "http", + "MaxAudioChannels": "6" }, { "Container": "wav", "Type": "Audio", "AudioCodec": "wav", - "Protocol": "http", - "EstimateContentLength": false, - "EnableMpegtsM2TsMode": false, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": false, "Context": "Static", - "EnableSubtitlesInManifest": false, - "MaxAudioChannels": "6", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": false, - "$type": "TranscodingProfile" + "Protocol": "http", + "MaxAudioChannels": "6" }, { "Container": "mp4", "Type": "Video", - "AudioCodec": "aac,ac3,eac3,flac,alac", - "VideoCodec": "hevc,h264", + "AudioCodec": "aac,ac3,eac3,opus,flac,alac", + "VideoCodec": "hevc,h264,vp9", "Context": "Streaming", "Protocol": "hls", "MaxAudioChannels": "2", @@ -237,121 +186,170 @@ "MaxAudioChannels": "2", "MinSegments": "2", "BreakOnNonKeyFrames": true - }, - { - "Container": "webm", - "Type": "Video", - "AudioCodec": "vorbis", - "VideoCodec": "vp8,vpx", - "Context": "Streaming", - "Protocol": "http", - "MaxAudioChannels": "2" - }, - { - "Container": "mp4", - "Type": "Video", - "AudioCodec": "aac,mp3,ac3,eac3,flac,alac,vorbis", - "VideoCodec": "h264", - "Context": "Static", - "Protocol": "http" } ], + "ContainerProfiles": [], "CodecProfiles": [ { "Type": "Video", + "Container": "hls", + "SubContainer": "mp4", + "Codec": "h264", + "Conditions": [ + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "high|main|baseline|constrained baseline|high 10", + "IsRequired": false + } + ] + }, + { + "Type": "Video", + "Codec": "h264", "Conditions": [ { "Condition": "NotEquals", "Property": "IsAnamorphic", "Value": "true", - "IsRequired": false, - "$type": "ProfileCondition" + "IsRequired": false }, { "Condition": "EqualsAny", "Property": "VideoProfile", "Value": "high|main|baseline|constrained baseline", - "IsRequired": false, - "$type": "ProfileCondition" + "IsRequired": false + }, + { + "Condition": "EqualsAny", + "Property": "VideoRangeType", + "Value": "SDR", + "IsRequired": false }, { "Condition": "LessThanEqual", "Property": "VideoLevel", "Value": "52", - "IsRequired": false, - "$type": "ProfileCondition" + "IsRequired": false }, { "Condition": "NotEquals", "Property": "IsInterlaced", "Value": "true", - "IsRequired": false, - "$type": "ProfileCondition" + "IsRequired": false } - ], - "Codec": "h264", - "$type": "CodecProfile" + ] }, { "Type": "Video", + "Codec": "hevc", "Conditions": [ { "Condition": "NotEquals", "Property": "IsAnamorphic", "Value": "true", - "IsRequired": false, - "$type": "ProfileCondition" + "IsRequired": false }, { "Condition": "EqualsAny", "Property": "VideoProfile", "Value": "main|main 10", - "IsRequired": false, - "$type": "ProfileCondition" + "IsRequired": false + }, + { + "Condition": "EqualsAny", + "Property": "VideoRangeType", + "Value": "SDR|HDR10|HLG|DOVI|DOVIWithHDR10|DOVIWithHLG|DOVIWithSDR", + "IsRequired": false }, { "Condition": "LessThanEqual", "Property": "VideoLevel", "Value": "183", - "IsRequired": false, - "$type": "ProfileCondition" + "IsRequired": false }, { "Condition": "NotEquals", "Property": "IsInterlaced", "Value": "true", - "IsRequired": false, - "$type": "ProfileCondition" + "IsRequired": false + }, + { + "Condition": "EqualsAny", + "Property": "VideoCodecTag", + "Value": "hvc1|dvh1", + "IsRequired": true + }, + { + "Condition": "LessThanEqual", + "Property": "VideoFramerate", + "Value": "60", + "IsRequired": true } - ], - "Codec": "hevc", - "$type": "CodecProfile" - } - ], - "ResponseProfiles": [ + ] + }, { - "Container": "m4v", "Type": "Video", - "MimeType": "video/mp4", - "$type": "ResponseProfile" + "Codec": "vp9", + "Conditions": [ + { + "Condition": "EqualsAny", + "Property": "VideoRangeType", + "Value": "SDR|HDR10|HLG", + "IsRequired": false + } + ] + }, + { + "Type": "Video", + "Codec": "av1", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "main", + "IsRequired": false + }, + { + "Condition": "EqualsAny", + "Property": "VideoRangeType", + "Value": "SDR|HDR10|HLG", + "IsRequired": false + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "15", + "IsRequired": false + } + ] } ], "SubtitleProfiles": [ { "Format": "vtt", - "Method": "External", - "$type": "SubtitleProfile" + "Method": "External" }, { "Format": "ass", - "Method": "External", - "$type": "SubtitleProfile" + "Method": "External" }, { "Format": "ssa", - "Method": "External", - "$type": "SubtitleProfile" + "Method": "External" } ], - "$type": "DeviceProfile" + "ResponseProfiles": [ + { + "Type": "Video", + "Container": "m4v", + "MimeType": "video/mp4" + } + ] } diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-hi10p-aac-5000k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-hi10p-aac-5000k.json new file mode 100644 index 0000000000..1296bece5a --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-hi10p-aac-5000k.json @@ -0,0 +1,86 @@ +{ + "Protocol": "File", + "Id": "a6e78000340509437325708e41b9e3bb", + "Path": "/Media/hi10p.mp4", + "Type": "Default", + "Container": "mov", + "Size": 58211635, + "Name": "MyVideo-hi10p", + "IsRemote": false, + "ETag": "8ad487e37ce9578122bbd8c42be2a392", + "RunTimeTicks": 920115000, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "VideoType": "VideoFile", + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "und", + "TimeBase": "1/16000", + "VideoRange": "SDR", + "VideoRangeType": "SDR", + "AudioSpatialFormat": "None", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "4", + "IsInterlaced": false, + "IsAVC": true, + "BitRate": 4820299, + "BitDepth": 10, + "RefFrames": 1, + "IsDefault": true, + "IsForced": false, + "IsHearingImpaired": false, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 24.007952, + "RealFrameRate": 23.976025, + "ReferenceFrameRate": 24.007952, + "Profile": "High 10", + "Type": "Video", + "AspectRatio": "16:9", + "Index": 0, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "PixelFormat": "yuv420p10le", + "Level": 51, + "IsAnamorphic": false + }, + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "und", + "TimeBase": "1/48000", + "VideoRange": "Unknown", + "VideoRangeType": "Unknown", + "AudioSpatialFormat": "None", + "LocalizedDefault": "Default", + "LocalizedExternal": "External", + "DisplayTitle": "AAC - Stereo - Default", + "IsInterlaced": false, + "IsAVC": false, + "ChannelLayout": "stereo", + "BitRate": 257358, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "IsForced": false, + "IsHearingImpaired": false, + "Profile": "LC", + "Type": "Audio", + "Index": 1, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "Level": 0 + } + ], + "MediaAttachments": [], + "Formats": [], + "Bitrate": 5061248, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": -1, + "HasSegments": false +} From de8bb15c7854858e4391982e72fe6eb686a6fd87 Mon Sep 17 00:00:00 2001 From: gnattu Date: Fri, 13 Sep 2024 18:19:05 +0800 Subject: [PATCH 056/159] Return more precise transcoding reasons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using the first profile's reason is somewhat arbitrary, as many clients' first profile may not be the most compatible one. For instance, browsers often set WebM as the first profile, which doesn’t support common codecs like H.264 and AAC by design. This causes `VideoCodecNotSupported` and `AudioCodecNotSupported` to be returned, even if the browser supports those codecs. Only use those reasons when all profiles indicate that the codec is not supported. Signed-off-by: gnattu --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 41 +++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index e4492ac79b..3354838c47 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1415,16 +1415,49 @@ namespace MediaBrowser.Model.Dlna return profileMatch; } + TranscodeReason inferredReason = 0; + var failureReasons = analyzedProfiles[false] .Select(analysis => analysis.Result) .Where(result => !containerSupported || (result.TranscodeReason & TranscodeReason.ContainerNotSupported) == 0) - .FirstOrDefault().TranscodeReason; - if (failureReasons == 0) + .Select(result => result.TranscodeReason) + .ToList(); + + if (failureReasons.FirstOrDefault() == 0) { - failureReasons = TranscodeReason.DirectPlayError; + inferredReason = TranscodeReason.DirectPlayError; + } + else + { + var videoCodecNotSupportedCount = failureReasons.Count(r => (r & TranscodeReason.VideoCodecNotSupported) != 0); + var audioCodecNotSupportedCount = failureReasons.Count(r => (r & TranscodeReason.AudioCodecNotSupported) != 0); + + if (!containerSupported) + { + inferredReason |= TranscodeReason.ContainerNotSupported; + } + + if (videoCodecNotSupportedCount == failureReasons.Count) + { + inferredReason |= TranscodeReason.VideoCodecNotSupported; + } + + if (audioCodecNotSupportedCount == failureReasons.Count) + { + inferredReason |= TranscodeReason.AudioCodecNotSupported; + } + + foreach (var transcodeReason in failureReasons) + { + var temp = transcodeReason; + temp &= ~TranscodeReason.ContainerNotSupported; + temp &= ~TranscodeReason.VideoCodecNotSupported; + temp &= ~TranscodeReason.AudioCodecNotSupported; + inferredReason |= temp; + } } - return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons); + return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: inferredReason); } private TranscodeReason CheckVideoAudioStreamDirectPlay(MediaOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream) From edc15c8e923610c7074328f1950d18f01926d552 Mon Sep 17 00:00:00 2001 From: gnattu Date: Fri, 13 Sep 2024 18:20:07 +0800 Subject: [PATCH 057/159] Add broken fps mkv test Signed-off-by: gnattu --- .../Dlna/StreamBuilderTests.cs | 6 +- ...fo-mkv-h264-hi10p-aac-5000k-brokenfps.json | 82 +++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-h264-hi10p-aac-5000k-brokenfps.json diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 9953431d92..297073166e 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -33,6 +33,7 @@ namespace Jellyfin.Model.Tests [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("Chrome", "mp4-h264-hi10p-aac-5000k", PlayMethod.DirectPlay)] + [InlineData("Chrome", "mkv-h264-hi10p-aac-5000k-brokenfps", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported, "Remux", "HLS.mp4")] // Firefox [InlineData("Firefox", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 @@ -45,7 +46,8 @@ namespace Jellyfin.Model.Tests [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported, "Remux", "HLS.mp4")] // #6450 [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450 - [InlineData("Firefox", "mp4-h264-hi10p-aac-5000k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")] // TODO: investigate why firefox profile has everything unsupported + [InlineData("Firefox", "mp4-h264-hi10p-aac-5000k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoProfileNotSupported, "Transcode", "HLS.mp4")] + [InlineData("Firefox", "mkv-h264-hi10p-aac-5000k-brokenfps", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoProfileNotSupported, "Transcode", "HLS.mp4")] // Safari [InlineData("SafariNext", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("SafariNext", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 @@ -56,7 +58,7 @@ namespace Jellyfin.Model.Tests [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 [InlineData("SafariNext", "mp4-h264-hi10p-aac-5000k", PlayMethod.Transcode, TranscodeReason.VideoProfileNotSupported, "Remux", "HLS.mp4")] - + [InlineData("SafariNext", "mkv-h264-hi10p-aac-5000k-brokenfps", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoProfileNotSupported, "Remux", "HLS.mp4")] // AndroidPixel [InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("AndroidPixel", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-h264-hi10p-aac-5000k-brokenfps.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-h264-hi10p-aac-5000k-brokenfps.json new file mode 100644 index 0000000000..b2dda6c5d4 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-h264-hi10p-aac-5000k-brokenfps.json @@ -0,0 +1,82 @@ +{ + "Protocol": "File", + "Id": "a6e78000340509437325708e41b9e3bb", + "Path": "/Media/hi10p.mkv", + "Type": "Default", + "Container": "mkv", + "Size": 58211635, + "Name": "MyVideo-hi10p-brokenfps", + "IsRemote": false, + "ETag": "60c03cb8a315fb6538439d3bb7e6944b", + "RunTimeTicks": 920115000, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "VideoType": "VideoFile", + "MediaStreams": [ + { + "Codec": "h264", + "TimeBase": "1/1000", + "VideoRange": "SDR", + "VideoRangeType": "SDR", + "AudioSpatialFormat": "None", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "4", + "IsInterlaced": false, + "IsAVC": true, + "BitRate": 5075104, + "BitDepth": 10, + "RefFrames": 1, + "IsDefault": true, + "IsForced": false, + "IsHearingImpaired": false, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 1000, + "RealFrameRate": 23.976025, + "ReferenceFrameRate": 23.976025, + "Profile": "High 10", + "Type": "Video", + "AspectRatio": "16:9", + "Index": 0, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "PixelFormat": "yuv420p10le", + "Level": 51, + "IsAnamorphic": false + }, + { + "Codec": "aac", + "TimeBase": "1/1000", + "VideoRange": "Unknown", + "VideoRangeType": "Unknown", + "AudioSpatialFormat": "None", + "LocalizedDefault": "Default", + "LocalizedExternal": "External", + "DisplayTitle": "AAC - Stereo - Default", + "IsInterlaced": false, + "IsAVC": false, + "ChannelLayout": "stereo", + "BitRate": 192000, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "IsForced": false, + "IsHearingImpaired": false, + "Profile": "LC", + "Type": "Audio", + "Index": 1, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "Level": 0 + } + ], + "MediaAttachments": [], + "Formats": [], + "Bitrate": 5061248, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": -1, + "HasSegments": false +} From 5913db991bca9628d7147df688874adae428ad74 Mon Sep 17 00:00:00 2001 From: gnattu Date: Fri, 13 Sep 2024 22:46:36 +0800 Subject: [PATCH 058/159] Improve readability Signed-off-by: gnattu --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 3354838c47..7789eac693 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1419,7 +1419,7 @@ namespace MediaBrowser.Model.Dlna var failureReasons = analyzedProfiles[false] .Select(analysis => analysis.Result) - .Where(result => !containerSupported || (result.TranscodeReason & TranscodeReason.ContainerNotSupported) == 0) + .Where(result => !containerSupported || !result.TranscodeReason.HasFlag(TranscodeReason.ContainerNotSupported)) .Select(result => result.TranscodeReason) .ToList(); @@ -1429,8 +1429,8 @@ namespace MediaBrowser.Model.Dlna } else { - var videoCodecNotSupportedCount = failureReasons.Count(r => (r & TranscodeReason.VideoCodecNotSupported) != 0); - var audioCodecNotSupportedCount = failureReasons.Count(r => (r & TranscodeReason.AudioCodecNotSupported) != 0); + var videoCodecNotSupportedCount = failureReasons.Count(r => r.HasFlag(TranscodeReason.VideoCodecNotSupported)); + var audioCodecNotSupportedCount = failureReasons.Count(r => r.HasFlag(TranscodeReason.AudioCodecNotSupported)); if (!containerSupported) { From cefcbcb2ac2f631e841c26b912113623d344a422 Mon Sep 17 00:00:00 2001 From: gnattu Date: Fri, 13 Sep 2024 23:17:33 +0800 Subject: [PATCH 059/159] Add mkv h264 ac3 tests Signed-off-by: gnattu --- .../Dlna/StreamBuilderTests.cs | 3 + ...ediaSourceInfo-mkv-h264-ac3-srt-2600k.json | 71 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-h264-ac3-srt-2600k.json diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 297073166e..d491e687c3 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -26,6 +26,7 @@ namespace Jellyfin.Model.Tests [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "HLS.mp4")] // #6450 [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioIsExternal, "DirectStream", "HLS.mp4")] // #6450 [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("Chrome", "mkv-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)] [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] [InlineData("Chrome", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "HLS.mp4")] @@ -40,6 +41,7 @@ namespace Jellyfin.Model.Tests [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "HLS.mp4")] // #6450 [InlineData("Firefox", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioIsExternal, "DirectStream", "HLS.mp4")] // #6450 [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("Firefox", "mkv-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "HLS.mp4")] [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")] [InlineData("Firefox", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode", "HLS.mp4")] @@ -54,6 +56,7 @@ namespace Jellyfin.Model.Tests [InlineData("SafariNext", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mkv-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioChannelsNotSupported, "Remux", "HLS.mp4")] // #6450 [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported, "Remux", "HLS.mp4")] // #6450 [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-h264-ac3-srt-2600k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-h264-ac3-srt-2600k.json new file mode 100644 index 0000000000..4f6d5bf000 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-h264-ac3-srt-2600k.json @@ -0,0 +1,71 @@ +{ + "Id": "a766d122b58e45d9492d17af77748bf5", + "Path": "/Media/MyVideo-720p.mkv", + "Container": "mkv", + "Size": 835317696, + "Name": "MyVideo-720p", + "ETag": "579a34c6d5dfb21d81539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "High", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 41 + }, + { + "Codec": "ac3", + "CodecTag": "ac-3", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - Dolby Digital - 5.1 - Default", + "ChannelLayout": "5.1", + "BitRate": 384000, + "Channels": 6, + "SampleRate": 48000, + "IsDefault": true, + "Index": 1, + "Score": 202 + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} From af92b4370f82be19622d41c4f464805b33b480c5 Mon Sep 17 00:00:00 2001 From: gnattu Date: Fri, 13 Sep 2024 23:19:35 +0800 Subject: [PATCH 060/159] Fix safari test Signed-off-by: gnattu --- tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index d491e687c3..8768d50a1f 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -56,7 +56,7 @@ namespace Jellyfin.Model.Tests [InlineData("SafariNext", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 - [InlineData("SafariNext", "mkv-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioChannelsNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mkv-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported, "Remux", "HLS.mp4")] // #6450 [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 From b0e6c357f706e1181be74321ea1af075bb375be7 Mon Sep 17 00:00:00 2001 From: gnattu Date: Fri, 13 Sep 2024 23:26:48 +0800 Subject: [PATCH 061/159] Restore progressive transcoding tests Signed-off-by: gnattu --- .../Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 8768d50a1f..0e4c130025 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -167,6 +167,18 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] + // Non-HLS Progressive transcoding + [InlineData("Chrome-NoHLS", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "http")] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "http")] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioIsExternal, "Remux", "http")] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "http")] // #6450 + [InlineData("Chrome-NoHLS", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")] + [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "http")] + [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode", "http")] + [InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported, "DirectStream", "http")] // webm requested, aac not supported + [InlineData("Chrome-NoHLS", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "http")] // #6450 + [InlineData("Chrome-NoHLS", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux", "http")] // #6450 public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = default, string transcodeMode = "DirectStream", string transcodeProtocol = "") { var options = await GetMediaOptions(deviceName, mediaSource); From 1e94511f794342dc37c0a8b290f99c518912dd9c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 15:31:51 +0000 Subject: [PATCH 062/159] Update github/codeql-action action to v3.26.7 --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 513139ea57..de5503b843 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '8.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 + uses: github/codeql-action/init@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 + uses: github/codeql-action/autobuild@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 + uses: github/codeql-action/analyze@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 From 118c583bff65453fe3999cbe2e6bddf3483ed703 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sat, 14 Sep 2024 01:23:41 +0800 Subject: [PATCH 063/159] Add Dolby Vision testing Signed-off-by: gnattu --- .../Dlna/StreamBuilderTests.cs | 19 +- .../Test Data/DeviceProfile-WebOS-23.json | 355 ++++++++++++++++++ ...diaSourceInfo-mkv-dvhe.05-eac3-28000k.json | 95 +++++ ...diaSourceInfo-mkv-dvhe.08-eac3-15200k.json | 101 +++++ ...diaSourceInfo-mp4-dvh1.05-eac3-15200k.json | 94 +++++ ...diaSourceInfo-mp4-dvhe.08-eac3-15200k.json | 97 +++++ 6 files changed, 760 insertions(+), 1 deletion(-) create mode 100644 tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-WebOS-23.json create mode 100644 tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.05-eac3-28000k.json create mode 100644 tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.08-eac3-15200k.json create mode 100644 tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-dvh1.05-eac3-15200k.json create mode 100644 tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-dvhe.08-eac3-15200k.json diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 0e4c130025..241f2a3830 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -35,6 +35,10 @@ namespace Jellyfin.Model.Tests [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("Chrome", "mp4-h264-hi10p-aac-5000k", PlayMethod.DirectPlay)] [InlineData("Chrome", "mkv-h264-hi10p-aac-5000k-brokenfps", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported, "Remux", "HLS.mp4")] + [InlineData("Chrome", "mp4-dvh1.05-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")] + [InlineData("Chrome", "mkv-dvhe.05-eac3-28000k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")] + [InlineData("Chrome", "mkv-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")] + [InlineData("Chrome", "mp4-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")] // Firefox [InlineData("Firefox", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 @@ -50,6 +54,10 @@ namespace Jellyfin.Model.Tests [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("Firefox", "mp4-h264-hi10p-aac-5000k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoProfileNotSupported, "Transcode", "HLS.mp4")] [InlineData("Firefox", "mkv-h264-hi10p-aac-5000k-brokenfps", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoProfileNotSupported, "Transcode", "HLS.mp4")] + [InlineData("Firefox", "mp4-dvh1.05-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")] + [InlineData("Firefox", "mkv-dvhe.05-eac3-28000k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")] + [InlineData("Firefox", "mkv-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")] + [InlineData("Firefox", "mp4-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")] // Safari [InlineData("SafariNext", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("SafariNext", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 @@ -62,6 +70,10 @@ namespace Jellyfin.Model.Tests [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 [InlineData("SafariNext", "mp4-h264-hi10p-aac-5000k", PlayMethod.Transcode, TranscodeReason.VideoProfileNotSupported, "Remux", "HLS.mp4")] [InlineData("SafariNext", "mkv-h264-hi10p-aac-5000k-brokenfps", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoProfileNotSupported, "Remux", "HLS.mp4")] + [InlineData("SafariNext", "mp4-dvh1.05-eac3-15200k", PlayMethod.DirectPlay)] + [InlineData("SafariNext", "mkv-dvhe.05-eac3-28000k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] + [InlineData("SafariNext", "mkv-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] + [InlineData("SafariNext", "mp4-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecTagNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // AndroidPixel [InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("AndroidPixel", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 @@ -167,6 +179,11 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] + // WebOS 23 + [InlineData("WebOS-23", "mkv-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoRangeTypeNotSupported, "Remux")] + [InlineData("WebOS-23", "mp4-dvh1.05-eac3-15200k", PlayMethod.DirectPlay)] + [InlineData("WebOS-23", "mp4-dvhe.08-eac3-15200k", PlayMethod.DirectPlay)] + [InlineData("WebOS-23", "mkv-dvhe.05-eac3-28000k", PlayMethod.Transcode, TranscodeReason.VideoRangeTypeNotSupported, "Remux")] // Non-HLS Progressive transcoding [InlineData("Chrome-NoHLS", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "http")] // #6450 @@ -422,7 +439,7 @@ namespace Jellyfin.Model.Tests // Full transcode if (transcodeMode.Equals("Transcode", StringComparison.Ordinal)) { - if ((streamInfo.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == 0) + if ((streamInfo.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError | TranscodeReason.VideoRangeTypeNotSupported)) == 0) { Assert.All( videoStreams, diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-WebOS-23.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-WebOS-23.json new file mode 100644 index 0000000000..094b0723b1 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-WebOS-23.json @@ -0,0 +1,355 @@ +{ + "MaxStreamingBitrate": 120000000, + "MaxStaticBitrate": 100000000, + "MusicStreamingTranscodingBitrate": 384000, + "DirectPlayProfiles": [ + { + "Container": "webm", + "Type": "Video", + "VideoCodec": "vp8,vp9,av1", + "AudioCodec": "vorbis,opus" + }, + { + "Container": "mp4,m4v", + "Type": "Video", + "VideoCodec": "h264,hevc,mpeg2video,vc1,vp9,av1", + "AudioCodec": "aac,ac3,eac3,mp2,pcm_s16le,pcm_s24le,opus,flac,vorbis" + }, + { + "Container": "mkv", + "Type": "Video", + "VideoCodec": "h264,hevc,mpeg2video,vc1,vp9,av1", + "AudioCodec": "aac,ac3,eac3,mp2,pcm_s16le,pcm_s24le,opus,flac,vorbis" + }, + { + "Container": "m2ts", + "Type": "Video", + "VideoCodec": "h264,vc1,mpeg2video", + "AudioCodec": "aac,ac3,eac3,mp2,pcm_s16le,pcm_s24le,opus,flac,vorbis" + }, + { + "Container": "wmv", + "Type": "Video", + "VideoCodec": "", + "AudioCodec": "" + }, + { + "Container": "ts,mpegts", + "Type": "Video", + "VideoCodec": "h264,hevc,vc1,mpeg2video", + "AudioCodec": "aac,ac3,eac3,mp2,pcm_s16le,pcm_s24le,opus,flac,vorbis" + }, + { + "Container": "asf", + "Type": "Video", + "VideoCodec": "", + "AudioCodec": "" + }, + { + "Container": "avi", + "Type": "Video", + "VideoCodec": "", + "AudioCodec": "aac,ac3,eac3,mp2,pcm_s16le,pcm_s24le,opus,flac,vorbis" + }, + { + "Container": "mpg", + "Type": "Video", + "VideoCodec": "", + "AudioCodec": "aac,ac3,eac3,mp2,pcm_s16le,pcm_s24le,opus,flac,vorbis" + }, + { + "Container": "mpeg", + "Type": "Video", + "VideoCodec": "", + "AudioCodec": "aac,ac3,eac3,mp2,pcm_s16le,pcm_s24le,opus,flac,vorbis" + }, + { + "Container": "mov", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac,ac3,eac3,mp2,pcm_s16le,pcm_s24le,opus,flac,vorbis" + }, + { + "Container": "opus", + "Type": "Audio" + }, + { + "Container": "webm", + "AudioCodec": "opus", + "Type": "Audio" + }, + { + "Container": "ts", + "AudioCodec": "mp3", + "Type": "Audio" + }, + { + "Container": "mp3", + "Type": "Audio" + }, + { + "Container": "aac", + "Type": "Audio" + }, + { + "Container": "m4a", + "AudioCodec": "aac", + "Type": "Audio" + }, + { + "Container": "m4b", + "AudioCodec": "aac", + "Type": "Audio" + }, + { + "Container": "mp4", + "AudioCodec": "flac", + "Type": "Audio" + }, + { + "Container": "webma", + "Type": "Audio" + }, + { + "Container": "webm", + "AudioCodec": "webma", + "Type": "Audio" + }, + { + "Container": "wav", + "Type": "Audio" + }, + { + "Container": "hls", + "Type": "Video", + "VideoCodec": "h264,hevc", + "AudioCodec": "aac,ac3,eac3,mp2" + } + ], + "TranscodingProfiles": [ + { + "Container": "ts", + "Type": "Audio", + "AudioCodec": "aac", + "Context": "Streaming", + "Protocol": "hls", + "MaxAudioChannels": "6", + "MinSegments": "1", + "BreakOnNonKeyFrames": false, + "EnableAudioVbrEncoding": true + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Context": "Streaming", + "Protocol": "http", + "MaxAudioChannels": "6" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Context": "Streaming", + "Protocol": "http", + "MaxAudioChannels": "6" + }, + { + "Container": "opus", + "Type": "Audio", + "AudioCodec": "opus", + "Context": "Streaming", + "Protocol": "http", + "MaxAudioChannels": "6" + }, + { + "Container": "wav", + "Type": "Audio", + "AudioCodec": "wav", + "Context": "Streaming", + "Protocol": "http", + "MaxAudioChannels": "6" + }, + { + "Container": "opus", + "Type": "Audio", + "AudioCodec": "opus", + "Context": "Static", + "Protocol": "http", + "MaxAudioChannels": "6" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Context": "Static", + "Protocol": "http", + "MaxAudioChannels": "6" + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Context": "Static", + "Protocol": "http", + "MaxAudioChannels": "6" + }, + { + "Container": "wav", + "Type": "Audio", + "AudioCodec": "wav", + "Context": "Static", + "Protocol": "http", + "MaxAudioChannels": "6" + }, + { + "Container": "ts", + "Type": "Video", + "AudioCodec": "aac,ac3,eac3,mp2", + "VideoCodec": "h264,hevc", + "Context": "Streaming", + "Protocol": "hls", + "MaxAudioChannels": "6", + "MinSegments": "1", + "BreakOnNonKeyFrames": false + } + ], + "ContainerProfiles": [], + "CodecProfiles": [ + { + "Type": "VideoAudio", + "Codec": "flac", + "Conditions": [ + { + "Condition": "LessThanEqual", + "Property": "AudioChannels", + "Value": "2", + "IsRequired": false + } + ] + }, + { + "Type": "Video", + "Codec": "h264", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "high|main|baseline|constrained baseline", + "IsRequired": false + }, + { + "Condition": "EqualsAny", + "Property": "VideoRangeType", + "Value": "SDR", + "IsRequired": false + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "52", + "IsRequired": false + } + ] + }, + { + "Type": "Video", + "Container": "-mp4,ts", + "Codec": "hevc", + "Conditions": [ + { + "Condition": "EqualsAny", + "Property": "VideoRangeType", + "Value": "SDR|HDR10|HLG", + "IsRequired": false + } + ] + }, + { + "Type": "Video", + "Codec": "hevc", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "main|main 10", + "IsRequired": false + }, + { + "Condition": "EqualsAny", + "Property": "VideoRangeType", + "Value": "SDR|HDR10|HLG|DOVI|DOVIWithHDR10|DOVIWithHLG|DOVIWithSDR", + "IsRequired": false + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "183", + "IsRequired": false + } + ] + }, + { + "Type": "Video", + "Codec": "vp9", + "Conditions": [ + { + "Condition": "EqualsAny", + "Property": "VideoRangeType", + "Value": "SDR|HDR10|HLG", + "IsRequired": false + } + ] + }, + { + "Type": "Video", + "Codec": "av1", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "main", + "IsRequired": false + }, + { + "Condition": "EqualsAny", + "Property": "VideoRangeType", + "Value": "SDR|HDR10|HLG", + "IsRequired": false + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "15", + "IsRequired": false + } + ] + } + ], + "SubtitleProfiles": [], + "ResponseProfiles": [ + { + "Type": "Video", + "Container": "m4v", + "MimeType": "video/mp4" + } + ] +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.05-eac3-28000k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.05-eac3-28000k.json new file mode 100644 index 0000000000..2fdd332769 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.05-eac3-28000k.json @@ -0,0 +1,95 @@ +{ + "Id": "e313fd4bfdfcab326b1fea833cffd779", + "Path": "/Media/MyVideo-dovi-p5.mkv", + "Type": "Default", + "Container": "mkv", + "Size": 199246498, + "Name": "MyVideo-dovi-p5", + "IsRemote": false, + "ETag": "3c932ee1cd94e3fecebcc3fac15053e9", + "RunTimeTicks": 562000000, + "SupportsTranscoding": true, + "SupportsDirectStream": false, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "VideoType": "VideoFile", + "MediaStreams": [ + { + "Codec": "hevc", + "CodecTag": "dvhe", + "Language": "und", + "DvVersionMajor": 1, + "DvVersionMinor": 0, + "DvProfile": 5, + "DvLevel": 9, + "RpuPresentFlag": 1, + "ElPresentFlag": 0, + "BlPresentFlag": 1, + "DvBlSignalCompatibilityId": 0, + "TimeBase": "1/60000", + "VideoRange": "HDR", + "VideoRangeType": "DOVI", + "VideoDoViTitle": "Dolby Vision Profile 5", + "AudioSpatialFormat": "None", + "DisplayTitle": "4K HEVC Dolby Vision Profile 5", + "IsInterlaced": false, + "IsAVC": false, + "BitRate": 27713921, + "BitDepth": 10, + "RefFrames": 1, + "IsDefault": true, + "IsForced": false, + "IsHearingImpaired": false, + "Height": 2160, + "Width": 3840, + "AverageFrameRate": 60, + "RealFrameRate": 60, + "ReferenceFrameRate": 60, + "Profile": "Main 10", + "Type": "Video", + "AspectRatio": "16:9", + "Index": 0, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "PixelFormat": "yuv420p10le", + "Level": 153, + "IsAnamorphic": false + }, + { + "Codec": "eac3", + "CodecTag": "ec-3", + "Language": "und", + "TimeBase": "1/48000", + "Title": "sound handler", + "VideoRange": "Unknown", + "VideoRangeType": "Unknown", + "AudioSpatialFormat": "DolbyAtmos", + "LocalizedDefault": "Default", + "LocalizedExternal": "External", + "DisplayTitle": "sound handler - Dolby Digital Plus + Dolby Atmos - 5.1 - Default", + "IsInterlaced": false, + "IsAVC": false, + "ChannelLayout": "5.1", + "BitRate": 640000, + "Channels": 6, + "SampleRate": 48000, + "IsDefault": true, + "IsForced": false, + "IsHearingImpaired": false, + "Profile": "Dolby Digital Plus + Dolby Atmos", + "Type": "Audio", + "Index": 1, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "Level": 0 + } + ], + "MediaAttachments": [], + "Bitrate": 28362490, + "RequiredHttpHeaders": {}, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": -1, + "HasSegments": false +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.08-eac3-15200k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.08-eac3-15200k.json new file mode 100644 index 0000000000..74c492c2bb --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.08-eac3-15200k.json @@ -0,0 +1,101 @@ +{ + "Protocol": "File", + "Id": "ac2a9824755fbeffd891b8ff2634901a", + "Path": "/Media/MyVideo-dovi-p8.mkv", + "Type": "Default", + "Container": "mkv", + "Size": 344509829, + "Name": "MyVideo-dovi-p8", + "ETag": "8ac40cacc99e4748bc9218045b38d184", + "RunTimeTicks": 1781120000, + "SupportsTranscoding": true, + "SupportsDirectStream": false, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "VideoType": "VideoFile", + "MediaStreams": [ + { + "Codec": "hevc", + "CodecTag": "hev1", + "Language": "und", + "ColorSpace": "bt2020nc", + "ColorTransfer": "smpte2084", + "ColorPrimaries": "bt2020", + "DvVersionMajor": 1, + "DvVersionMinor": 0, + "DvProfile": 8, + "DvLevel": 5, + "RpuPresentFlag": 1, + "ElPresentFlag": 0, + "BlPresentFlag": 1, + "DvBlSignalCompatibilityId": 1, + "TimeBase": "1/60000", + "VideoRange": "HDR", + "VideoRangeType": "DOVIWithHDR10", + "VideoDoViTitle": "Dolby Vision Profile 8.1 (HDR10)", + "AudioSpatialFormat": "None", + "DisplayTitle": "1080p HEVC Dolby Vision Profile 8.1 (HDR10)", + "IsInterlaced": false, + "IsAVC": false, + "BitRate": 15091058, + "BitDepth": 10, + "RefFrames": 1, + "IsDefault": true, + "IsForced": false, + "IsHearingImpaired": false, + "Height": 1080, + "Width": 1920, + "AverageFrameRate": 59.94006, + "RealFrameRate": 59.94006, + "ReferenceFrameRate": 59.94006, + "Profile": "Main 10", + "Type": "Video", + "AspectRatio": "16:9", + "Index": 0, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "PixelFormat": "yuv420p10le", + "Level": 153, + "IsAnamorphic": false + }, + { + "Codec": "eac3", + "CodecTag": "ec-3", + "Language": "und", + "TimeBase": "1/48000", + "Title": "Bento4 Sound Handler", + "VideoRange": "Unknown", + "VideoRangeType": "Unknown", + "AudioSpatialFormat": "DolbyAtmos", + "LocalizedDefault": "Default", + "LocalizedExternal": "External", + "DisplayTitle": "Bento4 Sound Handler - Dolby Digital Plus + Dolby Atmos - 5.1 - Default", + "IsInterlaced": false, + "IsAVC": false, + "ChannelLayout": "5.1", + "BitRate": 640000, + "Channels": 6, + "SampleRate": 48000, + "IsDefault": true, + "IsForced": false, + "IsHearingImpaired": false, + "Profile": "Dolby Digital Plus + Dolby Atmos", + "Type": "Audio", + "Index": 1, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "Level": 0 + } + ], + "MediaAttachments": [], + "Formats": [], + "Bitrate": 15473851, + "RequiredHttpHeaders": {}, + "TranscodingUrl": "/videos/ac2a9824-755f-beff-d891-b8ff2634901a/master.m3u8?DeviceId=TW96aWxsYS81LjAgKE1hY2ludG9zaDsgSW50ZWwgTWFjIE9TIFggMTBfMTVfNykgQXBwbGVXZWJLaXQvNjA1LjEuMTUgKEtIVE1MLCBsaWtlIEdlY2tvKSBWZXJzaW9uLzE3LjQgU2FmYXJpLzYwNS4xLjE1fDE3MTgxMjcxNTczNzk1&MediaSourceId=ac2a9824755fbeffd891b8ff2634901a&VideoCodec=hevc,h264,vp9,hevc&AudioCodec=eac3&AudioStreamIndex=1&VideoBitrate=148965748&AudioBitrate=640000&AudioSampleRate=48000&MaxFramerate=59.94006&PlaySessionId=2c5377dde2b944b18f80c7f3203e970f&api_key=f17a653e8c0c4b588f26231812ff3794&TranscodingMaxAudioChannels=6&RequireAvc=false&EnableAudioVbrEncoding=true&Tag=8ac40cacc99e4748bc9218045b38d184&SegmentContainer=mp4&MinSegments=2&BreakOnNonKeyFrames=True&hevc-level=153&hevc-videobitdepth=10&hevc-profile=main10&hevc-audiochannels=6&eac3-profile=dolbydigitalplus+dolbyatmos&vp9-rangetype=SDR,HDR10,HLG&hevc-rangetype=SDR,HDR10,HLG,DOVI,DOVIWithHDR10,DOVIWithHLG,DOVIWithSDR&hevc-deinterlace=true&hevc-codectag=hvc1,dvh1&h264-profile=high,main,baseline,constrainedbaseline,high10&h264-rangetype=SDR&h264-level=52&h264-deinterlace=true&TranscodeReasons=VideoCodecTagNotSupported", + "TranscodingSubProtocol": "hls", + "TranscodingContainer": "mp4", + "DefaultAudioStreamIndex": 1, + "HasSegments": false +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-dvh1.05-eac3-15200k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-dvh1.05-eac3-15200k.json new file mode 100644 index 0000000000..96e3caffc3 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-dvh1.05-eac3-15200k.json @@ -0,0 +1,94 @@ +{ + "Id": "a5365160a83cb0c518cc1c9ead31dbc7", + "Path": "/Media/MyVideo-dovi-p5.mp4", + "Type": "Default", + "Container": "mp4", + "Size": 345485021, + "Name": "MyVideo-dovi-p5", + "IsRemote": false, + "ETag": "a1aa7e722b9af5125b7387d0f58d463e", + "RunTimeTicks": 1781120000, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "VideoType": "VideoFile", + "MediaStreams": [ + { + "Codec": "hevc", + "CodecTag": "dvh1", + "Language": "und", + "DvVersionMajor": 1, + "DvVersionMinor": 0, + "DvProfile": 5, + "DvLevel": 5, + "RpuPresentFlag": 1, + "ElPresentFlag": 0, + "BlPresentFlag": 1, + "DvBlSignalCompatibilityId": 0, + "TimeBase": "1/60000", + "VideoRange": "HDR", + "VideoRangeType": "DOVI", + "VideoDoViTitle": "Dolby Vision Profile 5", + "AudioSpatialFormat": "None", + "DisplayTitle": "1080p HEVC Dolby Vision Profile 5", + "IsInterlaced": false, + "IsAVC": false, + "BitRate": 15135631, + "BitDepth": 10, + "RefFrames": 1, + "IsDefault": true, + "IsForced": false, + "IsHearingImpaired": false, + "Height": 1080, + "Width": 1920, + "AverageFrameRate": 59.94006, + "RealFrameRate": 59.94006, + "ReferenceFrameRate": 59.94006, + "Profile": "Main 10", + "Type": "Video", + "AspectRatio": "16:9", + "Index": 0, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "PixelFormat": "yuv420p10le", + "Level": 153, + "IsAnamorphic": false + }, + { + "Codec": "eac3", + "CodecTag": "ec-3", + "Language": "und", + "TimeBase": "1/48000", + "Title": "Bento4 Sound Handler", + "VideoRange": "Unknown", + "VideoRangeType": "Unknown", + "AudioSpatialFormat": "DolbyAtmos", + "LocalizedDefault": "Default", + "LocalizedExternal": "External", + "DisplayTitle": "Bento4 Sound Handler - Dolby Digital Plus + Dolby Atmos - 5.1 - Default", + "IsInterlaced": false, + "IsAVC": false, + "ChannelLayout": "5.1", + "BitRate": 640000, + "Channels": 6, + "SampleRate": 48000, + "IsDefault": true, + "IsForced": false, + "IsHearingImpaired": false, + "Profile": "Dolby Digital Plus + Dolby Atmos", + "Type": "Audio", + "Index": 1, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "Level": 0 + } + ], + "MediaAttachments": [], + "Bitrate": 15517652, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": -1, + "HasSegments": false +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-dvhe.08-eac3-15200k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-dvhe.08-eac3-15200k.json new file mode 100644 index 0000000000..6f77a8805e --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-dvhe.08-eac3-15200k.json @@ -0,0 +1,97 @@ +{ + "Protocol": "File", + "Id": "ac2a9824755fbeffd891b8ff2634901a", + "Path": "/Media/MyVideo-dovi-p8.mp4", + "Type": "Default", + "Container": "mp4", + "Size": 344509829, + "Name": "MyVideo-dovi-p8", + "ETag": "8ac40cacc99e4748bc9218045b38d184", + "RunTimeTicks": 1781120000, + "SupportsTranscoding": true, + "SupportsDirectStream": false, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "VideoType": "VideoFile", + "MediaStreams": [ + { + "Codec": "hevc", + "CodecTag": "hev1", + "Language": "und", + "ColorSpace": "bt2020nc", + "ColorTransfer": "smpte2084", + "ColorPrimaries": "bt2020", + "DvVersionMajor": 1, + "DvVersionMinor": 0, + "DvProfile": 8, + "DvLevel": 5, + "RpuPresentFlag": 1, + "ElPresentFlag": 0, + "BlPresentFlag": 1, + "DvBlSignalCompatibilityId": 1, + "TimeBase": "1/60000", + "VideoRange": "HDR", + "VideoRangeType": "DOVIWithHDR10", + "VideoDoViTitle": "Dolby Vision Profile 8.1 (HDR10)", + "AudioSpatialFormat": "None", + "DisplayTitle": "1080p HEVC Dolby Vision Profile 8.1 (HDR10)", + "IsInterlaced": false, + "IsAVC": false, + "BitRate": 15091058, + "BitDepth": 10, + "RefFrames": 1, + "IsDefault": true, + "IsForced": false, + "IsHearingImpaired": false, + "Height": 1080, + "Width": 1920, + "AverageFrameRate": 59.94006, + "RealFrameRate": 59.94006, + "ReferenceFrameRate": 59.94006, + "Profile": "Main 10", + "Type": "Video", + "AspectRatio": "16:9", + "Index": 0, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "PixelFormat": "yuv420p10le", + "Level": 153, + "IsAnamorphic": false + }, + { + "Codec": "eac3", + "CodecTag": "ec-3", + "Language": "und", + "TimeBase": "1/48000", + "Title": "Bento4 Sound Handler", + "VideoRange": "Unknown", + "VideoRangeType": "Unknown", + "AudioSpatialFormat": "DolbyAtmos", + "LocalizedDefault": "Default", + "LocalizedExternal": "External", + "DisplayTitle": "Bento4 Sound Handler - Dolby Digital Plus + Dolby Atmos - 5.1 - Default", + "IsInterlaced": false, + "IsAVC": false, + "ChannelLayout": "5.1", + "BitRate": 640000, + "Channels": 6, + "SampleRate": 48000, + "IsDefault": true, + "IsForced": false, + "IsHearingImpaired": false, + "Profile": "Dolby Digital Plus + Dolby Atmos", + "Type": "Audio", + "Index": 1, + "IsExternal": false, + "IsTextSubtitleStream": false, + "SupportsExternalStream": false, + "Level": 0 + } + ], + "MediaAttachments": [], + "Formats": [], + "Bitrate": 15473851, + "DefaultAudioStreamIndex": 1, + "HasSegments": false +} From 6a5f22fc2f965d8dcfbcf8ab361365f8d7be253d Mon Sep 17 00:00:00 2001 From: gnattu Date: Sat, 14 Sep 2024 01:24:57 +0800 Subject: [PATCH 064/159] Revert "Return more precise transcoding reasons" This reverts commit de8bb15c Signed-off-by: gnattu --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 41 +++--------------------- 1 file changed, 4 insertions(+), 37 deletions(-) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 7789eac693..86443affac 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1415,49 +1415,16 @@ namespace MediaBrowser.Model.Dlna return profileMatch; } - TranscodeReason inferredReason = 0; - var failureReasons = analyzedProfiles[false] .Select(analysis => analysis.Result) .Where(result => !containerSupported || !result.TranscodeReason.HasFlag(TranscodeReason.ContainerNotSupported)) - .Select(result => result.TranscodeReason) - .ToList(); - - if (failureReasons.FirstOrDefault() == 0) + .FirstOrDefault().TranscodeReason; + if (failureReasons == 0) { - inferredReason = TranscodeReason.DirectPlayError; - } - else - { - var videoCodecNotSupportedCount = failureReasons.Count(r => r.HasFlag(TranscodeReason.VideoCodecNotSupported)); - var audioCodecNotSupportedCount = failureReasons.Count(r => r.HasFlag(TranscodeReason.AudioCodecNotSupported)); - - if (!containerSupported) - { - inferredReason |= TranscodeReason.ContainerNotSupported; - } - - if (videoCodecNotSupportedCount == failureReasons.Count) - { - inferredReason |= TranscodeReason.VideoCodecNotSupported; - } - - if (audioCodecNotSupportedCount == failureReasons.Count) - { - inferredReason |= TranscodeReason.AudioCodecNotSupported; - } - - foreach (var transcodeReason in failureReasons) - { - var temp = transcodeReason; - temp &= ~TranscodeReason.ContainerNotSupported; - temp &= ~TranscodeReason.VideoCodecNotSupported; - temp &= ~TranscodeReason.AudioCodecNotSupported; - inferredReason |= temp; - } + failureReasons = TranscodeReason.DirectPlayError; } - return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: inferredReason); + return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons); } private TranscodeReason CheckVideoAudioStreamDirectPlay(MediaOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream) From 77c6fd5ab2a76a6c442debe20735cff610d460a5 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Sat, 14 Sep 2024 01:26:51 +0800 Subject: [PATCH 065/159] Improve direct profile ranking --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 86443affac..bf612f0ac0 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -19,8 +19,10 @@ namespace MediaBrowser.Model.Dlna { // Aliases internal const TranscodeReason ContainerReasons = TranscodeReason.ContainerNotSupported | TranscodeReason.ContainerBitrateExceedsLimit; - internal const TranscodeReason AudioReasons = TranscodeReason.AudioCodecNotSupported | TranscodeReason.AudioBitrateNotSupported | TranscodeReason.AudioChannelsNotSupported | TranscodeReason.AudioProfileNotSupported | TranscodeReason.AudioSampleRateNotSupported | TranscodeReason.SecondaryAudioNotSupported | TranscodeReason.AudioBitDepthNotSupported | TranscodeReason.AudioIsExternal; - internal const TranscodeReason VideoReasons = TranscodeReason.VideoCodecNotSupported | TranscodeReason.VideoResolutionNotSupported | TranscodeReason.AnamorphicVideoNotSupported | TranscodeReason.InterlacedVideoNotSupported | TranscodeReason.VideoBitDepthNotSupported | TranscodeReason.VideoBitrateNotSupported | TranscodeReason.VideoFramerateNotSupported | TranscodeReason.VideoLevelNotSupported | TranscodeReason.RefFramesNotSupported | TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.VideoProfileNotSupported; + internal const TranscodeReason AudioCodecReasons = TranscodeReason.AudioBitrateNotSupported | TranscodeReason.AudioChannelsNotSupported | TranscodeReason.AudioProfileNotSupported | TranscodeReason.AudioSampleRateNotSupported | TranscodeReason.SecondaryAudioNotSupported | TranscodeReason.AudioBitDepthNotSupported | TranscodeReason.AudioIsExternal; + internal const TranscodeReason AudioReasons = TranscodeReason.AudioCodecNotSupported | AudioCodecReasons; + internal const TranscodeReason VideoCodecReasons = TranscodeReason.VideoResolutionNotSupported | TranscodeReason.AnamorphicVideoNotSupported | TranscodeReason.InterlacedVideoNotSupported | TranscodeReason.VideoBitDepthNotSupported | TranscodeReason.VideoBitrateNotSupported | TranscodeReason.VideoFramerateNotSupported | TranscodeReason.VideoLevelNotSupported | TranscodeReason.RefFramesNotSupported | TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.VideoProfileNotSupported; + internal const TranscodeReason VideoReasons = TranscodeReason.VideoCodecNotSupported | VideoCodecReasons; internal const TranscodeReason DirectStreamReasons = AudioReasons | TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecTagNotSupported; private readonly ILogger _logger; @@ -1314,7 +1316,7 @@ namespace MediaBrowser.Model.Dlna } } - var rankings = new[] { VideoReasons, AudioReasons, ContainerReasons }; + var rankings = new[] { TranscodeReason.VideoCodecNotSupported, VideoCodecReasons, TranscodeReason.AudioCodecNotSupported, AudioCodecReasons, ContainerReasons }; var rank = (ref TranscodeReason a) => { var index = 1; From 3d43b834de34b248c18a905d2a490f8522101904 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sat, 14 Sep 2024 01:34:06 +0800 Subject: [PATCH 066/159] Remove redundant info --- .../Test Data/MediaSourceInfo-mkv-dvhe.08-eac3-15200k.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.08-eac3-15200k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.08-eac3-15200k.json index 74c492c2bb..c4197fe314 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.08-eac3-15200k.json +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mkv-dvhe.08-eac3-15200k.json @@ -92,10 +92,6 @@ "MediaAttachments": [], "Formats": [], "Bitrate": 15473851, - "RequiredHttpHeaders": {}, - "TranscodingUrl": "/videos/ac2a9824-755f-beff-d891-b8ff2634901a/master.m3u8?DeviceId=TW96aWxsYS81LjAgKE1hY2ludG9zaDsgSW50ZWwgTWFjIE9TIFggMTBfMTVfNykgQXBwbGVXZWJLaXQvNjA1LjEuMTUgKEtIVE1MLCBsaWtlIEdlY2tvKSBWZXJzaW9uLzE3LjQgU2FmYXJpLzYwNS4xLjE1fDE3MTgxMjcxNTczNzk1&MediaSourceId=ac2a9824755fbeffd891b8ff2634901a&VideoCodec=hevc,h264,vp9,hevc&AudioCodec=eac3&AudioStreamIndex=1&VideoBitrate=148965748&AudioBitrate=640000&AudioSampleRate=48000&MaxFramerate=59.94006&PlaySessionId=2c5377dde2b944b18f80c7f3203e970f&api_key=f17a653e8c0c4b588f26231812ff3794&TranscodingMaxAudioChannels=6&RequireAvc=false&EnableAudioVbrEncoding=true&Tag=8ac40cacc99e4748bc9218045b38d184&SegmentContainer=mp4&MinSegments=2&BreakOnNonKeyFrames=True&hevc-level=153&hevc-videobitdepth=10&hevc-profile=main10&hevc-audiochannels=6&eac3-profile=dolbydigitalplus+dolbyatmos&vp9-rangetype=SDR,HDR10,HLG&hevc-rangetype=SDR,HDR10,HLG,DOVI,DOVIWithHDR10,DOVIWithHLG,DOVIWithSDR&hevc-deinterlace=true&hevc-codectag=hvc1,dvh1&h264-profile=high,main,baseline,constrainedbaseline,high10&h264-rangetype=SDR&h264-level=52&h264-deinterlace=true&TranscodeReasons=VideoCodecTagNotSupported", - "TranscodingSubProtocol": "hls", - "TranscodingContainer": "mp4", "DefaultAudioStreamIndex": 1, "HasSegments": false } From ffbfd46dea6fa5d19ce446d51f238cc9b531862f Mon Sep 17 00:00:00 2001 From: gnattu Date: Sat, 14 Sep 2024 03:28:14 +0800 Subject: [PATCH 067/159] Move progressive tests to old place --- .../Dlna/StreamBuilderTests.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 241f2a3830..7b4bb05ff1 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -107,6 +107,18 @@ namespace Jellyfin.Model.Tests [InlineData("JellyfinMediaPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("JellyfinMediaPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("JellyfinMediaPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + // Non-HLS Progressive transcoding + [InlineData("Chrome-NoHLS", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "http")] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "http")] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioIsExternal, "Remux", "http")] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "http")] // #6450 + [InlineData("Chrome-NoHLS", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")] + [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "http")] + [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode", "http")] + [InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported, "DirectStream", "http")] // webm requested, aac not supported + [InlineData("Chrome-NoHLS", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "http")] // #6450 + [InlineData("Chrome-NoHLS", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux", "http")] // #6450 // TranscodeMedia [InlineData("TranscodeMedia", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")] [InlineData("TranscodeMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "HLS.mp4")] @@ -184,18 +196,6 @@ namespace Jellyfin.Model.Tests [InlineData("WebOS-23", "mp4-dvh1.05-eac3-15200k", PlayMethod.DirectPlay)] [InlineData("WebOS-23", "mp4-dvhe.08-eac3-15200k", PlayMethod.DirectPlay)] [InlineData("WebOS-23", "mkv-dvhe.05-eac3-28000k", PlayMethod.Transcode, TranscodeReason.VideoRangeTypeNotSupported, "Remux")] - // Non-HLS Progressive transcoding - [InlineData("Chrome-NoHLS", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 - [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "http")] // #6450 - [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux", "http")] // #6450 - [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioIsExternal, "Remux", "http")] // #6450 - [InlineData("Chrome-NoHLS", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "http")] // #6450 - [InlineData("Chrome-NoHLS", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")] - [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "http")] - [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode", "http")] - [InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported, "DirectStream", "http")] // webm requested, aac not supported - [InlineData("Chrome-NoHLS", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "http")] // #6450 - [InlineData("Chrome-NoHLS", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux", "http")] // #6450 public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = default, string transcodeMode = "DirectStream", string transcodeProtocol = "") { var options = await GetMediaOptions(deviceName, mediaSource); From 195142861c195131dfb13b86f3b5cbbbe640356d Mon Sep 17 00:00:00 2001 From: NonameMissingNo Date: Sat, 14 Sep 2024 12:34:21 +0000 Subject: [PATCH 068/159] Translated using Weblate (Hungarian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hu/ --- Emby.Server.Implementations/Localization/Core/hu.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index 31d6aaedb9..2c8533ac65 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -130,5 +130,8 @@ "TaskAudioNormalization": "Hangerő Normalizáció", "TaskCleanCollectionsAndPlaylistsDescription": "Nem létező elemek törlése a gyűjteményekből és lejátszási listákról.", "TaskAudioNormalizationDescription": "Hangerő normalizációs adatok keresése.", - "TaskCleanCollectionsAndPlaylists": "Gyűjtemények és lejátszási listák optimalizálása" + "TaskCleanCollectionsAndPlaylists": "Gyűjtemények és lejátszási listák optimalizálása", + "TaskExtractMediaSegments": "Média szegmens felismerése", + "TaskDownloadMissingLyrics": "Hiányzó szöveg letöltése", + "TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése" } From 7df938674e08cf0387965866735dd398cbc4cb6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A3o=20jos=C3=A9?= Date: Fri, 13 Sep 2024 23:36:03 +0000 Subject: [PATCH 069/159] Translated using Weblate (Portuguese (Brazil)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_BR/ --- Emby.Server.Implementations/Localization/Core/pt-BR.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index 0c9f4c1710..9bc012ae5a 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -132,5 +132,9 @@ "TaskAudioNormalization": "Normalização de áudio", "TaskAudioNormalizationDescription": "Examina os ficheiros em busca de dados de normalização de áudio.", "TaskDownloadMissingLyricsDescription": "Baixar letras para músicas", - "TaskDownloadMissingLyrics": "Baixar letra faltante" + "TaskDownloadMissingLyrics": "Baixar letra faltante", + "TaskMoveTrickplayImagesDescription": "Move os arquivos do trickplay de acordo com as configurações da biblioteca.", + "TaskExtractMediaSegments": "Varredura do segmento de mídia", + "TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de mídia de plug-ins habilitados para MediaSegment.", + "TaskMoveTrickplayImages": "Migrar o local da imagem do Trickplay" } From 66bfb2b4f81b0a3a5fb8671254e5a658be715091 Mon Sep 17 00:00:00 2001 From: xwr Date: Fri, 13 Sep 2024 17:36:21 +0000 Subject: [PATCH 070/159] Translated using Weblate (Galician) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/gl/ --- Emby.Server.Implementations/Localization/Core/gl.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json index 76a98aa54b..3ba3e6679e 100644 --- a/Emby.Server.Implementations/Localization/Core/gl.json +++ b/Emby.Server.Implementations/Localization/Core/gl.json @@ -1,7 +1,7 @@ { "Albums": "Álbumes", - "Collections": "Colecións", - "ChapterNameValue": "Capítulos {0}", + "Collections": "Coleccións", + "ChapterNameValue": "Capítulo {0}", "Channels": "Canles", "CameraImageUploadedFrom": "Cargouse unha nova imaxe da cámara desde {0}", "Books": "Libros", From 523e0c927eeac074604cb625331423e801039d75 Mon Sep 17 00:00:00 2001 From: Filipe Motta Date: Sun, 15 Sep 2024 22:57:49 +0000 Subject: [PATCH 071/159] Translated using Weblate (Portuguese (Brazil)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_BR/ --- Emby.Server.Implementations/Localization/Core/pt-BR.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index 9bc012ae5a..9f4f58cb69 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -8,7 +8,7 @@ "CameraImageUploadedFrom": "Uma nova imagem da câmera foi enviada de {0}", "Channels": "Canais", "ChapterNameValue": "Capítulo {0}", - "Collections": "Coletâneas", + "Collections": "Coleções", "DeviceOfflineWithName": "{0} se desconectou", "DeviceOnlineWithName": "{0} se conectou", "FailedLoginAttemptWithUserName": "Falha na tentativa de login de {0}", From 2a6f7c1a40af972a6b5101f11e258eba459d331c Mon Sep 17 00:00:00 2001 From: sand14 Date: Sun, 15 Sep 2024 00:34:54 +0000 Subject: [PATCH 072/159] Translated using Weblate (Romanian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ro/ --- Emby.Server.Implementations/Localization/Core/ro.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json index 2f52aafa36..bf59e15837 100644 --- a/Emby.Server.Implementations/Localization/Core/ro.json +++ b/Emby.Server.Implementations/Localization/Core/ro.json @@ -129,5 +129,11 @@ "TaskAudioNormalizationDescription": "Scanează fișiere pentru date necesare normalizării sunetului.", "TaskAudioNormalization": "Normalizare sunet", "TaskCleanCollectionsAndPlaylists": "Curăță colecțiile și listele de redare", - "TaskCleanCollectionsAndPlaylistsDescription": "Elimină elementele care nu mai există din colecții și liste de redare." + "TaskCleanCollectionsAndPlaylistsDescription": "Elimină elementele care nu mai există din colecții și liste de redare.", + "TaskExtractMediaSegments": "Scanează segmentele media", + "TaskMoveTrickplayImagesDescription": "Mută fișierele trickplay existente conform setărilor librăriei.", + "TaskExtractMediaSegmentsDescription": "Extrage sau obține segmentele media de la pluginurile MediaSegment activate.", + "TaskMoveTrickplayImages": "Migrează locația imaginii Trickplay", + "TaskDownloadMissingLyrics": "Descarcă versurile lipsă", + "TaskDownloadMissingLyricsDescription": "Descarcă versuri pentru melodii" } From d3e7f53d93b7bfcc0a70c6bea069c6fb53e6c37e Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Tue, 17 Sep 2024 00:47:02 +0800 Subject: [PATCH 073/159] Fix some PGSSUB burn-in perf regressions (#12655) --- .../MediaEncoding/EncodingHelper.cs | 89 ++++++++++++++----- 1 file changed, 66 insertions(+), 23 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 5a4af8ce88..b186e3e535 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2980,6 +2980,8 @@ namespace MediaBrowser.Controller.MediaEncoding public static string GetGraphicalSubPreProcessFilters( int? videoWidth, int? videoHeight, + int? subtitleWidth, + int? subtitleHeight, int? requestedWidth, int? requestedHeight, int? requestedMaxWidth, @@ -2993,16 +2995,37 @@ namespace MediaBrowser.Controller.MediaEncoding requestedMaxWidth, requestedMaxHeight); - if (outWidth.HasValue && outHeight.HasValue) + if (!outWidth.HasValue + || !outHeight.HasValue + || outWidth.Value <= 0 + || outHeight.Value <= 0) { - return string.Format( - CultureInfo.InvariantCulture, - @"scale,scale=-1:{1}:fast_bilinear,crop,pad=max({0}\,iw):max({1}\,ih):(ow-iw)/2:(oh-ih)/2:black@0,crop={0}:{1}", - outWidth.Value, - outHeight.Value); + return string.Empty; } - return string.Empty; + // Automatically add padding based on subtitle input + var filters = @"scale,scale=-1:{1}:fast_bilinear,crop,pad=max({0}\,iw):max({1}\,ih):(ow-iw)/2:(oh-ih)/2:black@0,crop={0}:{1}"; + + if (subtitleWidth.HasValue + && subtitleHeight.HasValue + && subtitleWidth.Value > 0 + && subtitleHeight.Value > 0) + { + var videoDar = (double)outWidth.Value / outHeight.Value; + var subtitleDar = (double)subtitleWidth.Value / subtitleHeight.Value; + + // No need to add padding when DAR is the same -> 1080p PGSSUB on 2160p video + if (videoDar == subtitleDar) + { + filters = @"scale,scale={0}:{1}:fast_bilinear"; + } + } + + return string.Format( + CultureInfo.InvariantCulture, + filters, + outWidth.Value, + outHeight.Value); } public static string GetAlphaSrcFilter( @@ -3507,7 +3530,9 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } @@ -3580,6 +3605,8 @@ namespace MediaBrowser.Controller.MediaEncoding var hasAssSubs = hasSubs && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); @@ -3683,7 +3710,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); subFilters.Add("format=yuva420p"); } @@ -3708,7 +3735,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } @@ -3783,6 +3810,8 @@ namespace MediaBrowser.Controller.MediaEncoding var hasAssSubs = hasSubs && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); @@ -3897,7 +3926,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); subFilters.Add("format=yuva420p"); } @@ -3924,7 +3953,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } @@ -4021,6 +4050,8 @@ namespace MediaBrowser.Controller.MediaEncoding var hasAssSubs = hasSubs && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); @@ -4193,7 +4224,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (hasGraphicalSubs) { // overlay_qsv can handle overlay scaling, setup a smaller height to reduce transfer overhead - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, 1080); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, 1080); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -4229,7 +4260,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } @@ -4273,6 +4304,8 @@ namespace MediaBrowser.Controller.MediaEncoding var hasAssSubs = hasSubs && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); @@ -4439,7 +4472,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (hasGraphicalSubs) { // overlay_qsv can handle overlay scaling, setup a smaller height to reduce transfer overhead - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, 1080); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, 1080); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -4474,7 +4507,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } @@ -4584,6 +4617,8 @@ namespace MediaBrowser.Controller.MediaEncoding var hasAssSubs = hasSubs && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); @@ -4725,7 +4760,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (hasGraphicalSubs) { // overlay_vaapi can handle overlay scaling, setup a smaller height to reduce transfer overhead - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, 1080); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, 1080); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -4758,7 +4793,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); @@ -4950,7 +4985,9 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -5173,7 +5210,9 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); @@ -5324,7 +5363,9 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -5449,6 +5490,8 @@ namespace MediaBrowser.Controller.MediaEncoding var hasAssSubs = hasSubs && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + var subW = state.SubtitleStream?.Width; + var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); @@ -5583,7 +5626,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); subFilters.Add("format=bgra"); } @@ -5616,7 +5659,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (hasGraphicalSubs) { - var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var subPreProcFilters = GetGraphicalSubPreProcessFilters(swpInW, swpInH, subW, subH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subPreProcFilters); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } From b92fc7ea9dbf86437a981c3f0477a7b457977b9a Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 17 Sep 2024 00:47:12 +0800 Subject: [PATCH 074/159] Don't resolve trickplay folder during media scanning (#12652) --- Emby.Server.Implementations/Library/IgnorePatterns.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index a2301c8aed..bb45dd87e9 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -50,6 +50,10 @@ namespace Emby.Server.Implementations.Library "**/lost+found/**", "**/lost+found", + // Trickplay files + "**/*.trickplay", + "**/*.trickplay/**", + // WMC temp recording directories that will constantly be written to "**/TempRec/**", "**/TempRec", From 41ac5f8d76ce11a852e4dafbf20ad57d63d55f96 Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 17 Sep 2024 21:08:16 +0800 Subject: [PATCH 075/159] Fix subtitle dar comparison when number not exact (#12660) --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index b186e3e535..88aa888a1e 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -3015,7 +3015,7 @@ namespace MediaBrowser.Controller.MediaEncoding var subtitleDar = (double)subtitleWidth.Value / subtitleHeight.Value; // No need to add padding when DAR is the same -> 1080p PGSSUB on 2160p video - if (videoDar == subtitleDar) + if (Math.Abs(videoDar - subtitleDar) < 0.01f) { filters = @"scale,scale={0}:{1}:fast_bilinear"; } From 2351eeba561905bafae48a948f3126797c284766 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 17 Sep 2024 20:29:43 +0200 Subject: [PATCH 076/159] Rework PR 6203 --- .../Devices/DeviceManager.cs | 4 +- MediaBrowser.Model/Dlna/CodecProfile.cs | 136 +- MediaBrowser.Model/Dlna/ContainerProfile.cs | 107 +- MediaBrowser.Model/Dlna/DeviceProfile.cs | 109 +- MediaBrowser.Model/Dlna/DirectPlayProfile.cs | 85 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 194 +- MediaBrowser.Model/Dlna/StreamInfo.cs | 1983 ++++++++++------- MediaBrowser.Model/Dlna/SubtitleProfile.cs | 84 +- MediaBrowser.Model/Dlna/TranscodingProfile.cs | 196 +- .../Extensions/ContainerHelper.cs | 145 ++ .../MediaInfo/AudioFileProber.cs | 2 +- .../Dlna/ContainerHelperTests.cs | 54 + .../Dlna/ContainerProfileTests.cs | 19 - .../Dlna/StreamBuilderTests.cs | 21 +- 14 files changed, 1848 insertions(+), 1291 deletions(-) create mode 100644 MediaBrowser.Model/Extensions/ContainerHelper.cs create mode 100644 tests/Jellyfin.Model.Tests/Dlna/ContainerHelperTests.cs delete mode 100644 tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index d7a46e2d54..415c04bbf1 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -135,8 +135,8 @@ namespace Jellyfin.Server.Implementations.Devices { IEnumerable devices = _devices.Values .Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value)) - .Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId) - .Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken) + .Where(device => query.DeviceId is null || device.DeviceId == query.DeviceId) + .Where(device => query.AccessToken is null || device.AccessToken == query.AccessToken) .OrderBy(d => d.Id) .ToList(); var count = devices.Count(); diff --git a/MediaBrowser.Model/Dlna/CodecProfile.cs b/MediaBrowser.Model/Dlna/CodecProfile.cs index 07c1a29a4f..da34eddcd1 100644 --- a/MediaBrowser.Model/Dlna/CodecProfile.cs +++ b/MediaBrowser.Model/Dlna/CodecProfile.cs @@ -1,74 +1,94 @@ -#nullable disable -#pragma warning disable CS1591 - using System; +using System.Collections.Generic; +using System.Linq; using System.Xml.Serialization; -using Jellyfin.Extensions; +using MediaBrowser.Model.Extensions; -namespace MediaBrowser.Model.Dlna +namespace MediaBrowser.Model.Dlna; + +/// +/// Defines the . +/// +public class CodecProfile { - public class CodecProfile + /// + /// Initializes a new instance of the class. + /// + public CodecProfile() { - public CodecProfile() - { - Conditions = Array.Empty(); - ApplyConditions = Array.Empty(); - } + Conditions = []; + ApplyConditions = []; + } - [XmlAttribute("type")] - public CodecType Type { get; set; } + /// + /// Gets or sets the which this container must meet. + /// + [XmlAttribute("type")] + public CodecType Type { get; set; } - public ProfileCondition[] Conditions { get; set; } + /// + /// Gets or sets the list of which this profile must meet. + /// + public ProfileCondition[] Conditions { get; set; } - public ProfileCondition[] ApplyConditions { get; set; } + /// + /// Gets or sets the list of to apply if this profile is met. + /// + public ProfileCondition[] ApplyConditions { get; set; } - [XmlAttribute("codec")] - public string Codec { get; set; } + /// + /// Gets or sets the codec(s) that this profile applies to. + /// + [XmlAttribute("codec")] + public string? Codec { get; set; } - [XmlAttribute("container")] - public string Container { get; set; } + /// + /// Gets or sets the container(s) which this profile will be applied to. + /// + [XmlAttribute("container")] + public string? Container { get; set; } - [XmlAttribute("subcontainer")] - public string SubContainer { get; set; } + /// + /// Gets or sets the sub-container(s) which this profile will be applied to. + /// + [XmlAttribute("subcontainer")] + public string? SubContainer { get; set; } - public string[] GetCodecs() - { - return ContainerProfile.SplitValue(Codec); - } + /// + /// Checks to see whether the codecs and containers contain the given parameters. + /// + /// The codecs to match. + /// The container to match. + /// Consider sub-containers. + /// True if both conditions are met. + public bool ContainsAnyCodec(IReadOnlyList codecs, string? container, bool useSubContainer = false) + { + var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container; + return ContainerHelper.ContainsContainer(containerToCheck, container) && codecs.Any(c => ContainerHelper.ContainsContainer(Codec, false, c)); + } - private bool ContainsContainer(string container, bool useSubContainer = false) - { - var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container; - return ContainerProfile.ContainsContainer(containerToCheck, container); - } + /// + /// Checks to see whether the codecs and containers contain the given parameters. + /// + /// The codec to match. + /// The container to match. + /// Consider sub-containers. + /// True if both conditions are met. + public bool ContainsAnyCodec(string? codec, string? container, bool useSubContainer = false) + { + return ContainsAnyCodec(codec.AsSpan(), container, useSubContainer); + } - public bool ContainsAnyCodec(string codec, string container, bool useSubContainer = false) - { - return ContainsAnyCodec(ContainerProfile.SplitValue(codec), container, useSubContainer); - } - - public bool ContainsAnyCodec(string[] codec, string container, bool useSubContainer = false) - { - if (!ContainsContainer(container, useSubContainer)) - { - return false; - } - - var codecs = GetCodecs(); - if (codecs.Length == 0) - { - return true; - } - - foreach (var val in codec) - { - if (codecs.Contains(val, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } + /// + /// Checks to see whether the codecs and containers contain the given parameters. + /// + /// The codec to match. + /// The container to match. + /// Consider sub-containers. + /// True if both conditions are met. + public bool ContainsAnyCodec(ReadOnlySpan codec, string? container, bool useSubContainer = false) + { + var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container; + return ContainerHelper.ContainsContainer(containerToCheck, container) && ContainerHelper.ContainsContainer(Codec, false, codec); } } diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs index 9780042684..a421799075 100644 --- a/MediaBrowser.Model/Dlna/ContainerProfile.cs +++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs @@ -1,74 +1,49 @@ -#pragma warning disable CS1591 +#pragma warning disable CA1819 // Properties should not return arrays using System; +using System.Collections.Generic; using System.Xml.Serialization; -using Jellyfin.Extensions; +using MediaBrowser.Model.Extensions; -namespace MediaBrowser.Model.Dlna +namespace MediaBrowser.Model.Dlna; + +/// +/// Defines the . +/// +public class ContainerProfile { - public class ContainerProfile + /// + /// Gets or sets the which this container must meet. + /// + [XmlAttribute("type")] + public DlnaProfileType Type { get; set; } + + /// + /// Gets or sets the list of which this container will be applied to. + /// + public ProfileCondition[] Conditions { get; set; } = []; + + /// + /// Gets or sets the container(s) which this container must meet. + /// + [XmlAttribute("container")] + public string? Container { get; set; } + + /// + /// Gets or sets the sub container(s) which this container must meet. + /// + [XmlAttribute("subcontainer")] + public string? SubContainer { get; set; } + + /// + /// Returns true if an item in appears in the property. + /// + /// The item to match. + /// Consider subcontainers. + /// The result of the operation. + public bool ContainsContainer(ReadOnlySpan container, bool useSubContainer = false) { - [XmlAttribute("type")] - public DlnaProfileType Type { get; set; } - - public ProfileCondition[] Conditions { get; set; } = Array.Empty(); - - [XmlAttribute("container")] - public string Container { get; set; } = string.Empty; - - public static string[] SplitValue(string? value) - { - if (string.IsNullOrEmpty(value)) - { - return Array.Empty(); - } - - return value.Split(',', StringSplitOptions.RemoveEmptyEntries); - } - - public bool ContainsContainer(string? container) - { - var containers = SplitValue(Container); - - return ContainsContainer(containers, container); - } - - public static bool ContainsContainer(string? profileContainers, string? inputContainer) - { - var isNegativeList = false; - if (profileContainers is not null && profileContainers.StartsWith('-')) - { - isNegativeList = true; - profileContainers = profileContainers.Substring(1); - } - - return ContainsContainer(SplitValue(profileContainers), isNegativeList, inputContainer); - } - - public static bool ContainsContainer(string[]? profileContainers, string? inputContainer) - { - return ContainsContainer(profileContainers, false, inputContainer); - } - - public static bool ContainsContainer(string[]? profileContainers, bool isNegativeList, string? inputContainer) - { - if (profileContainers is null || profileContainers.Length == 0) - { - // Empty profiles always support all containers/codecs - return true; - } - - var allInputContainers = SplitValue(inputContainer); - - foreach (var container in allInputContainers) - { - if (profileContainers.Contains(container, StringComparison.OrdinalIgnoreCase)) - { - return !isNegativeList; - } - } - - return isNegativeList; - } + var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container; + return ContainerHelper.ContainsContainer(containerToCheck, container); } } diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs index 2addebbfca..f689576222 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfile.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -1,74 +1,71 @@ #pragma warning disable CA1819 // Properties should not return arrays using System; -using System.Xml.Serialization; -namespace MediaBrowser.Model.Dlna +namespace MediaBrowser.Model.Dlna; + +/// +/// A represents a set of metadata which determines which content a certain device is able to play. +///
+/// Specifically, it defines the supported containers and +/// codecs (video and/or audio, including codec profiles and levels) +/// the device is able to direct play (without transcoding or remuxing), +/// as well as which containers/codecs to transcode to in case it isn't. +///
+public class DeviceProfile { /// - /// A represents a set of metadata which determines which content a certain device is able to play. - ///
- /// Specifically, it defines the supported containers and - /// codecs (video and/or audio, including codec profiles and levels) - /// the device is able to direct play (without transcoding or remuxing), - /// as well as which containers/codecs to transcode to in case it isn't. + /// Gets or sets the name of this device profile. User profiles must have a unique name. ///
- public class DeviceProfile - { - /// - /// Gets or sets the name of this device profile. - /// - public string? Name { get; set; } + public string? Name { get; set; } - /// - /// Gets or sets the Id. - /// - [XmlIgnore] - public string? Id { get; set; } + /// + /// Gets or sets the unique internal identifier. + /// + public Guid Id { get; set; } - /// - /// Gets or sets the maximum allowed bitrate for all streamed content. - /// - public int? MaxStreamingBitrate { get; set; } = 8000000; + /// + /// Gets or sets the maximum allowed bitrate for all streamed content. + /// + public int? MaxStreamingBitrate { get; set; } = 8000000; - /// - /// Gets or sets the maximum allowed bitrate for statically streamed content (= direct played files). - /// - public int? MaxStaticBitrate { get; set; } = 8000000; + /// + /// Gets or sets the maximum allowed bitrate for statically streamed content (= direct played files). + /// + public int? MaxStaticBitrate { get; set; } = 8000000; - /// - /// Gets or sets the maximum allowed bitrate for transcoded music streams. - /// - public int? MusicStreamingTranscodingBitrate { get; set; } = 128000; + /// + /// Gets or sets the maximum allowed bitrate for transcoded music streams. + /// + public int? MusicStreamingTranscodingBitrate { get; set; } = 128000; - /// - /// Gets or sets the maximum allowed bitrate for statically streamed (= direct played) music files. - /// - public int? MaxStaticMusicBitrate { get; set; } = 8000000; + /// + /// Gets or sets the maximum allowed bitrate for statically streamed (= direct played) music files. + /// + public int? MaxStaticMusicBitrate { get; set; } = 8000000; - /// - /// Gets or sets the direct play profiles. - /// - public DirectPlayProfile[] DirectPlayProfiles { get; set; } = Array.Empty(); + /// + /// Gets or sets the direct play profiles. + /// + public DirectPlayProfile[] DirectPlayProfiles { get; set; } = []; - /// - /// Gets or sets the transcoding profiles. - /// - public TranscodingProfile[] TranscodingProfiles { get; set; } = Array.Empty(); + /// + /// Gets or sets the transcoding profiles. + /// + public TranscodingProfile[] TranscodingProfiles { get; set; } = []; - /// - /// Gets or sets the container profiles. - /// - public ContainerProfile[] ContainerProfiles { get; set; } = Array.Empty(); + /// + /// Gets or sets the container profiles. Failing to meet these optional conditions causes transcoding to occur. + /// + public ContainerProfile[] ContainerProfiles { get; set; } = []; - /// - /// Gets or sets the codec profiles. - /// - public CodecProfile[] CodecProfiles { get; set; } = Array.Empty(); + /// + /// Gets or sets the codec profiles. + /// + public CodecProfile[] CodecProfiles { get; set; } = []; - /// - /// Gets or sets the subtitle profiles. - /// - public SubtitleProfile[] SubtitleProfiles { get; set; } = Array.Empty(); - } + /// + /// Gets or sets the subtitle profiles. + /// + public SubtitleProfile[] SubtitleProfiles { get; set; } = []; } diff --git a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs index f68235d869..438df34415 100644 --- a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs +++ b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs @@ -1,36 +1,65 @@ -#pragma warning disable CS1591 - using System.Xml.Serialization; +using MediaBrowser.Model.Extensions; -namespace MediaBrowser.Model.Dlna +namespace MediaBrowser.Model.Dlna; + +/// +/// Defines the . +/// +public class DirectPlayProfile { - public class DirectPlayProfile + /// + /// Gets or sets the container. + /// + [XmlAttribute("container")] + public string Container { get; set; } = string.Empty; + + /// + /// Gets or sets the audio codec. + /// + [XmlAttribute("audioCodec")] + public string? AudioCodec { get; set; } + + /// + /// Gets or sets the video codec. + /// + [XmlAttribute("videoCodec")] + public string? VideoCodec { get; set; } + + /// + /// Gets or sets the Dlna profile type. + /// + [XmlAttribute("type")] + public DlnaProfileType Type { get; set; } + + /// + /// Returns whether the supports the . + /// + /// The container to match against. + /// True if supported. + public bool SupportsContainer(string? container) { - [XmlAttribute("container")] - public string? Container { get; set; } + return ContainerHelper.ContainsContainer(Container, container); + } - [XmlAttribute("audioCodec")] - public string? AudioCodec { get; set; } + /// + /// Returns whether the supports the . + /// + /// The codec to match against. + /// True if supported. + public bool SupportsVideoCodec(string? codec) + { + return Type == DlnaProfileType.Video && ContainerHelper.ContainsContainer(VideoCodec, codec); + } - [XmlAttribute("videoCodec")] - public string? VideoCodec { get; set; } - - [XmlAttribute("type")] - public DlnaProfileType Type { get; set; } - - public bool SupportsContainer(string? container) - { - return ContainerProfile.ContainsContainer(Container, container); - } - - public bool SupportsVideoCodec(string? codec) - { - return Type == DlnaProfileType.Video && ContainerProfile.ContainsContainer(VideoCodec, codec); - } - - public bool SupportsAudioCodec(string? codec) - { - return (Type == DlnaProfileType.Audio || Type == DlnaProfileType.Video) && ContainerProfile.ContainsContainer(AudioCodec, codec); - } + /// + /// Returns whether the supports the . + /// + /// The codec to match against. + /// True if supported. + public bool SupportsAudioCodec(string? codec) + { + // Video profiles can have audio codec restrictions too, therefore incude Video as valid type. + return (Type == DlnaProfileType.Audio || Type == DlnaProfileType.Video) && ContainerHelper.ContainsContainer(AudioCodec, codec); } } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index bf612f0ac0..6fc7f796de 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -6,6 +6,7 @@ using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; @@ -27,9 +28,9 @@ namespace MediaBrowser.Model.Dlna private readonly ILogger _logger; private readonly ITranscoderSupport _transcoderSupport; - private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc", "vp9", "av1" }; - private static readonly string[] _supportedHlsAudioCodecsTs = new string[] { "aac", "ac3", "eac3", "mp3" }; - private static readonly string[] _supportedHlsAudioCodecsMp4 = new string[] { "aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd" }; + private static readonly string[] _supportedHlsVideoCodecs = ["h264", "hevc", "vp9", "av1"]; + private static readonly string[] _supportedHlsAudioCodecsTs = ["aac", "ac3", "eac3", "mp3"]; + private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd"]; /// /// Initializes a new instance of the class. @@ -51,7 +52,7 @@ namespace MediaBrowser.Model.Dlna { ValidateMediaOptions(options, false); - var streams = new List(); + List streams = []; foreach (var mediaSource in options.MediaSources) { if (!(string.IsNullOrEmpty(options.MediaSourceId) @@ -64,7 +65,7 @@ namespace MediaBrowser.Model.Dlna if (streamInfo is not null) { streamInfo.DeviceId = options.DeviceId; - streamInfo.DeviceProfileId = options.Profile.Id; + streamInfo.DeviceProfileId = options.Profile.Id.ToString("N", CultureInfo.InvariantCulture); streams.Add(streamInfo); } } @@ -129,7 +130,7 @@ namespace MediaBrowser.Model.Dlna if (directPlayMethod is PlayMethod.DirectStream) { var remuxContainer = item.TranscodingContainer ?? "ts"; - var supportedHlsContainers = new[] { "ts", "mp4" }; + string[] supportedHlsContainers = ["ts", "mp4"]; // If the container specified for the profile is an HLS supported container, use that container instead, overriding the preference // The client should be responsible to ensure this container is compatible remuxContainer = Array.Exists(supportedHlsContainers, element => string.Equals(element, directPlayInfo.Profile?.Container, StringComparison.OrdinalIgnoreCase)) ? directPlayInfo.Profile?.Container : remuxContainer; @@ -226,7 +227,7 @@ namespace MediaBrowser.Model.Dlna ? options.MediaSources : options.MediaSources.Where(x => string.Equals(x.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)); - var streams = new List(); + List streams = []; foreach (var mediaSourceInfo in mediaSources) { var streamInfo = BuildVideoItem(mediaSourceInfo, options); @@ -239,7 +240,7 @@ namespace MediaBrowser.Model.Dlna foreach (var stream in streams) { stream.DeviceId = options.DeviceId; - stream.DeviceProfileId = options.Profile.Id; + stream.DeviceProfileId = options.Profile.Id.ToString("N", CultureInfo.InvariantCulture); } return GetOptimalStream(streams, options.GetMaxBitrate(false) ?? 0); @@ -388,32 +389,33 @@ namespace MediaBrowser.Model.Dlna /// The . /// The object to get the video stream from. /// The normalized input container. - public static string? NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile? profile, DlnaProfileType type, DirectPlayProfile? playProfile = null) + public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile? profile, DlnaProfileType type, DirectPlayProfile? playProfile = null) { - if (string.IsNullOrEmpty(inputContainer)) + if (profile is null || !inputContainer.Contains(',', StringComparison.OrdinalIgnoreCase)) { - return null; + return inputContainer; } - var formats = ContainerProfile.SplitValue(inputContainer); - - if (profile is not null) + var formats = ContainerHelper.Split(inputContainer); + var playProfiles = playProfile is null ? profile.DirectPlayProfiles : [playProfile]; + foreach (var format in formats) { - var playProfiles = playProfile is null ? profile.DirectPlayProfiles : new[] { playProfile }; - foreach (var format in formats) + foreach (var directPlayProfile in playProfiles) { - foreach (var directPlayProfile in playProfiles) + if (directPlayProfile.Type != type) { - if (directPlayProfile.Type == type - && directPlayProfile.SupportsContainer(format)) - { - return format; - } + continue; + } + + var formatStr = format.ToString(); + if (directPlayProfile.SupportsContainer(formatStr)) + { + return formatStr; } } } - return formats[0]; + return inputContainer; } private (DirectPlayProfile? Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, MediaOptions options) @@ -533,7 +535,6 @@ namespace MediaBrowser.Model.Dlna private static int? GetDefaultSubtitleStreamIndex(MediaSourceInfo item, SubtitleProfile[] subtitleProfiles) { int highestScore = -1; - foreach (var stream in item.MediaStreams) { if (stream.Type == MediaStreamType.Subtitle @@ -544,7 +545,7 @@ namespace MediaBrowser.Model.Dlna } } - var topStreams = new List(); + List topStreams = []; foreach (var stream in item.MediaStreams) { if (stream.Type == MediaStreamType.Subtitle && stream.Score.HasValue && stream.Score.Value == highestScore) @@ -623,8 +624,8 @@ namespace MediaBrowser.Model.Dlna playlistItem.Container = container; playlistItem.SubProtocol = protocol; - playlistItem.VideoCodecs = new[] { item.VideoStream.Codec }; - playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile?.AudioCodec); + playlistItem.VideoCodecs = [item.VideoStream.Codec]; + playlistItem.AudioCodecs = ContainerHelper.Split(directPlayProfile?.AudioCodec); } private StreamInfo BuildVideoItem(MediaSourceInfo item, MediaOptions options) @@ -651,7 +652,7 @@ namespace MediaBrowser.Model.Dlna } // Collect candidate audio streams - ICollection candidateAudioStreams = audioStream is null ? Array.Empty() : new[] { audioStream }; + ICollection candidateAudioStreams = audioStream is null ? [] : [audioStream]; if (!options.AudioStreamIndex.HasValue || options.AudioStreamIndex < 0) { if (audioStream?.IsDefault == true) @@ -702,7 +703,8 @@ namespace MediaBrowser.Model.Dlna directPlayProfile = directPlayInfo.Profile; playlistItem.PlayMethod = directPlay.Value; playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); - playlistItem.VideoCodecs = new[] { videoStream.Codec }; + var videoCodec = videoStream?.Codec; + playlistItem.VideoCodecs = videoCodec is null ? [] : [videoCodec]; if (directPlay == PlayMethod.DirectPlay) { @@ -713,7 +715,7 @@ namespace MediaBrowser.Model.Dlna { playlistItem.AudioStreamIndex = audioStreamIndex; var audioCodec = item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec; - playlistItem.AudioCodecs = audioCodec is null ? Array.Empty() : new[] { audioCodec }; + playlistItem.AudioCodecs = audioCodec is null ? [] : [audioCodec]; } } else if (directPlay == PlayMethod.DirectStream) @@ -721,7 +723,7 @@ namespace MediaBrowser.Model.Dlna playlistItem.AudioStreamIndex = audioStream?.Index; if (audioStream is not null) { - playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile?.AudioCodec); + playlistItem.AudioCodecs = ContainerHelper.Split(directPlayProfile?.AudioCodec); } SetStreamInfoOptionsFromDirectPlayProfile(options, item, playlistItem, directPlayProfile); @@ -753,7 +755,7 @@ namespace MediaBrowser.Model.Dlna { // Can't direct play, find the transcoding profile // If we do this for direct-stream we will overwrite the info - var (transcodingProfile, playMethod) = GetVideoTranscodeProfile(item, options, videoStream, audioStream, candidateAudioStreams, subtitleStream, playlistItem); + var (transcodingProfile, playMethod) = GetVideoTranscodeProfile(item, options, videoStream, audioStream, playlistItem); if (transcodingProfile is not null && playMethod.HasValue) { @@ -781,7 +783,7 @@ namespace MediaBrowser.Model.Dlna } playlistItem.SubtitleFormat = subtitleProfile.Format; - playlistItem.SubtitleCodecs = new[] { subtitleProfile.Format }; + playlistItem.SubtitleCodecs = [subtitleProfile.Format]; } if ((playlistItem.TranscodeReasons & (VideoReasons | TranscodeReason.ContainerBitrateExceedsLimit)) != 0) @@ -810,8 +812,6 @@ namespace MediaBrowser.Model.Dlna MediaOptions options, MediaStream? videoStream, MediaStream? audioStream, - IEnumerable candidateAudioStreams, - MediaStream? subtitleStream, StreamInfo playlistItem) { if (!(item.SupportsTranscoding || item.SupportsDirectStream)) @@ -849,9 +849,7 @@ namespace MediaBrowser.Model.Dlna if (options.AllowVideoStreamCopy) { - var videoCodecs = ContainerProfile.SplitValue(transcodingProfile.VideoCodec); - - if (ContainerProfile.ContainsContainer(videoCodecs, videoCodec)) + if (ContainerHelper.ContainsContainer(transcodingProfile.VideoCodec, videoCodec)) { var appliedVideoConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.Video && @@ -868,9 +866,7 @@ namespace MediaBrowser.Model.Dlna if (options.AllowAudioStreamCopy) { - var audioCodecs = ContainerProfile.SplitValue(transcodingProfile.AudioCodec); - - if (ContainerProfile.ContainsContainer(audioCodecs, audioCodec)) + if (ContainerHelper.ContainsContainer(transcodingProfile.AudioCodec, audioCodec)) { var appliedVideoConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.VideoAudio && @@ -913,20 +909,18 @@ namespace MediaBrowser.Model.Dlna string? audioCodec) { // Prefer matching video codecs - var videoCodecs = ContainerProfile.SplitValue(videoCodec); + var videoCodecs = ContainerHelper.Split(videoCodec).ToList(); + + if (videoCodecs.Count == 0 && videoStream is not null) + { + // Add the original codec if no codec is specified + videoCodecs.Add(videoStream.Codec); + } // Enforce HLS video codec restrictions if (playlistItem.SubProtocol == MediaStreamProtocol.hls) { - videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToArray(); - } - - var directVideoCodec = ContainerProfile.ContainsContainer(videoCodecs, videoStream?.Codec) ? videoStream?.Codec : null; - if (directVideoCodec is not null) - { - // merge directVideoCodec to videoCodecs - Array.Resize(ref videoCodecs, videoCodecs.Length + 1); - videoCodecs[^1] = directVideoCodec; + videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToList(); } playlistItem.VideoCodecs = videoCodecs; @@ -950,22 +944,28 @@ namespace MediaBrowser.Model.Dlna } // Prefer matching audio codecs, could do better here - var audioCodecs = ContainerProfile.SplitValue(audioCodec); + var audioCodecs = ContainerHelper.Split(audioCodec).ToList(); + + if (audioCodecs.Count == 0 && audioStream is not null) + { + // Add the original codec if no codec is specified + audioCodecs.Add(audioStream.Codec); + } // Enforce HLS audio codec restrictions if (playlistItem.SubProtocol == MediaStreamProtocol.hls) { if (string.Equals(playlistItem.Container, "mp4", StringComparison.OrdinalIgnoreCase)) { - audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsMp4.Contains(codec)).ToArray(); + audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsMp4.Contains(codec)).ToList(); } else { - audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsTs.Contains(codec)).ToArray(); + audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsTs.Contains(codec)).ToList(); } } - var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec)).FirstOrDefault(); + var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerHelper.ContainsContainer(audioCodecs, false, stream.Codec)).FirstOrDefault(); var directAudioStream = audioStreamWithSupportedCodec?.Channels is not null && audioStreamWithSupportedCodec.Channels.Value <= (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue) ? audioStreamWithSupportedCodec : null; @@ -982,7 +982,8 @@ namespace MediaBrowser.Model.Dlna { audioStream = directAudioStream; playlistItem.AudioStreamIndex = audioStream.Index; - playlistItem.AudioCodecs = audioCodecs = new[] { audioStream.Codec }; + audioCodecs = [audioStream.Codec]; + playlistItem.AudioCodecs = audioCodecs; // Copy matching audio codec options playlistItem.AudioSampleRate = audioStream.SampleRate; @@ -1023,18 +1024,17 @@ namespace MediaBrowser.Model.Dlna var appliedVideoConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.Video && - i.ContainsAnyCodec(videoCodecs, container, useSubContainer) && + i.ContainsAnyCodec(playlistItem.VideoCodecs, container, useSubContainer) && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))) // Reverse codec profiles for backward compatibility - first codec profile has higher priority .Reverse(); - - foreach (var i in appliedVideoConditions) + foreach (var condition in appliedVideoConditions) { - foreach (var transcodingVideoCodec in videoCodecs) + foreach (var transcodingVideoCodec in playlistItem.VideoCodecs) { - if (i.ContainsAnyCodec(transcodingVideoCodec, container, useSubContainer)) + if (condition.ContainsAnyCodec(transcodingVideoCodec, container, useSubContainer)) { - ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, true); + ApplyTranscodingConditions(playlistItem, condition.Conditions, transcodingVideoCodec, true, true); continue; } } @@ -1055,14 +1055,14 @@ namespace MediaBrowser.Model.Dlna var appliedAudioConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.VideoAudio && - i.ContainsAnyCodec(audioCodecs, container) && + i.ContainsAnyCodec(playlistItem.AudioCodecs, container) && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio))) // Reverse codec profiles for backward compatibility - first codec profile has higher priority .Reverse(); foreach (var codecProfile in appliedAudioConditions) { - foreach (var transcodingAudioCodec in audioCodecs) + foreach (var transcodingAudioCodec in playlistItem.AudioCodecs) { if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container)) { @@ -1132,9 +1132,9 @@ namespace MediaBrowser.Model.Dlna return 192000; } - private static int GetAudioBitrate(long maxTotalBitrate, string[] targetAudioCodecs, MediaStream? audioStream, StreamInfo item) + private static int GetAudioBitrate(long maxTotalBitrate, IReadOnlyList targetAudioCodecs, MediaStream? audioStream, StreamInfo item) { - string? targetAudioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0]; + string? targetAudioCodec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0]; int? targetAudioChannels = item.GetTargetAudioChannels(targetAudioCodec); @@ -1151,7 +1151,7 @@ namespace MediaBrowser.Model.Dlna && audioStream.Channels.HasValue && audioStream.Channels.Value > targetAudioChannels.Value) { - // Reduce the bitrate if we're downmixing. + // Reduce the bitrate if we're down mixing. defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, targetAudioChannels); } else if (targetAudioChannels.HasValue @@ -1159,8 +1159,8 @@ namespace MediaBrowser.Model.Dlna && audioStream.Channels.Value <= targetAudioChannels.Value && !string.IsNullOrEmpty(audioStream.Codec) && targetAudioCodecs is not null - && targetAudioCodecs.Length > 0 - && !Array.Exists(targetAudioCodecs, elem => string.Equals(audioStream.Codec, elem, StringComparison.OrdinalIgnoreCase))) + && targetAudioCodecs.Count > 0 + && !targetAudioCodecs.Any(elem => string.Equals(audioStream.Codec, elem, StringComparison.OrdinalIgnoreCase))) { // Shift the bitrate if we're transcoding to a different audio codec. defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, audioStream.Channels.Value); @@ -1299,7 +1299,7 @@ namespace MediaBrowser.Model.Dlna !checkVideoConditions(codecProfile.ApplyConditions).Any()) .SelectMany(codecProfile => checkVideoConditions(codecProfile.Conditions))); - // Check audiocandidates profile conditions + // Check audio candidates profile conditions var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => CheckVideoAudioStreamDirectPlay(options, mediaSource, container, audioStream)); TranscodeReason subtitleProfileReasons = 0; @@ -1316,24 +1316,6 @@ namespace MediaBrowser.Model.Dlna } } - var rankings = new[] { TranscodeReason.VideoCodecNotSupported, VideoCodecReasons, TranscodeReason.AudioCodecNotSupported, AudioCodecReasons, ContainerReasons }; - var rank = (ref TranscodeReason a) => - { - var index = 1; - foreach (var flag in rankings) - { - var reason = a & flag; - if (reason != 0) - { - return index; - } - - index++; - } - - return index; - }; - var containerSupported = false; // Check DirectPlay profiles to see if it can be direct played @@ -1400,7 +1382,9 @@ namespace MediaBrowser.Model.Dlna playMethod = PlayMethod.DirectStream; } - var ranked = rank(ref failureReasons); + TranscodeReason[] rankings = [TranscodeReason.VideoCodecNotSupported, VideoCodecReasons, TranscodeReason.AudioCodecNotSupported, AudioCodecReasons, ContainerReasons]; + var ranked = GetRank(ref failureReasons, rankings); + return (Result: (Profile: directPlayProfile, PlayMethod: playMethod, AudioStreamIndex: selectedAudioStream?.Index, TranscodeReason: failureReasons), Order: order, Rank: ranked); }) .OrderByDescending(analysis => analysis.Result.PlayMethod) @@ -1475,7 +1459,7 @@ namespace MediaBrowser.Model.Dlna /// The . /// The . /// The output container. - /// The subtitle transoding protocol. + /// The subtitle transcoding protocol. /// The normalized input container. public static SubtitleProfile GetSubtitleProfile( MediaSourceInfo mediaSource, @@ -1501,7 +1485,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (!ContainerProfile.ContainsContainer(profile.Container, outputContainer)) + if (!ContainerHelper.ContainsContainer(profile.Container, outputContainer)) { continue; } @@ -1530,7 +1514,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (!ContainerProfile.ContainsContainer(profile.Container, outputContainer)) + if (!ContainerHelper.ContainsContainer(profile.Container, outputContainer)) { continue; } @@ -1561,17 +1545,12 @@ namespace MediaBrowser.Model.Dlna { if (!string.IsNullOrEmpty(transcodingContainer)) { - string[] normalizedContainers = ContainerProfile.SplitValue(transcodingContainer); - - if (ContainerProfile.ContainsContainer(normalizedContainers, "ts") - || ContainerProfile.ContainsContainer(normalizedContainers, "mpegts") - || ContainerProfile.ContainsContainer(normalizedContainers, "mp4")) + if (ContainerHelper.ContainsContainer(transcodingContainer, "ts,mpegts,mp4")) { return false; } - if (ContainerProfile.ContainsContainer(normalizedContainers, "mkv") - || ContainerProfile.ContainsContainer(normalizedContainers, "matroska")) + if (ContainerHelper.ContainsContainer(transcodingContainer, "mkv,matroska")) { return true; } @@ -2274,5 +2253,22 @@ namespace MediaBrowser.Model.Dlna return false; } + + private int GetRank(ref TranscodeReason a, TranscodeReason[] rankings) + { + var index = 1; + foreach (var flag in rankings) + { + var reason = a & flag; + if (reason != 0) + { + return index; + } + + index++; + } + + return index; + } } } diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 8232ee3fe5..3be6860880 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -1,9 +1,6 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; using Jellyfin.Data.Enums; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; @@ -11,1007 +8,1303 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Session; -namespace MediaBrowser.Model.Dlna +namespace MediaBrowser.Model.Dlna; + +/// +/// Class holding information on a stream. +/// +public class StreamInfo { /// - /// Class StreamInfo. + /// Initializes a new instance of the class. /// - public class StreamInfo + public StreamInfo() { - public StreamInfo() + AudioCodecs = []; + VideoCodecs = []; + SubtitleCodecs = []; + StreamOptions = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + /// + /// Gets or sets the item id. + /// + /// The item id. + public Guid ItemId { get; set; } + + /// + /// Gets or sets the play method. + /// + /// The play method. + public PlayMethod PlayMethod { get; set; } + + /// + /// Gets or sets the encoding context. + /// + /// The encoding context. + public EncodingContext Context { get; set; } + + /// + /// Gets or sets the media type. + /// + /// The media type. + public DlnaProfileType MediaType { get; set; } + + /// + /// Gets or sets the container. + /// + /// The container. + public string? Container { get; set; } + + /// + /// Gets or sets the sub protocol. + /// + /// The sub protocol. + public MediaStreamProtocol SubProtocol { get; set; } + + /// + /// Gets or sets the start position ticks. + /// + /// The start position ticks. + public long StartPositionTicks { get; set; } + + /// + /// Gets or sets the segment length. + /// + /// The segment length. + public int? SegmentLength { get; set; } + + /// + /// Gets or sets the minimum segments count. + /// + /// The minimum segments count. + public int? MinSegments { get; set; } + + /// + /// Gets or sets a value indicating whether the stream can be broken on non-keyframes. + /// + public bool BreakOnNonKeyFrames { get; set; } + + /// + /// Gets or sets a value indicating whether the stream requires AVC. + /// + public bool RequireAvc { get; set; } + + /// + /// Gets or sets a value indicating whether the stream requires AVC. + /// + public bool RequireNonAnamorphic { get; set; } + + /// + /// Gets or sets a value indicating whether timestamps should be copied. + /// + public bool CopyTimestamps { get; set; } + + /// + /// Gets or sets a value indicating whether timestamps should be copied. + /// + public bool EnableMpegtsM2TsMode { get; set; } + + /// + /// Gets or sets a value indicating whether the subtitle manifest is enabled. + /// + public bool EnableSubtitlesInManifest { get; set; } + + /// + /// Gets or sets the audio codecs. + /// + /// The audio codecs. + public IReadOnlyList AudioCodecs { get; set; } + + /// + /// Gets or sets the video codecs. + /// + /// The video codecs. + public IReadOnlyList VideoCodecs { get; set; } + + /// + /// Gets or sets the audio stream index. + /// + /// The audio stream index. + public int? AudioStreamIndex { get; set; } + + /// + /// Gets or sets the video stream index. + /// + /// The subtitle stream index. + public int? SubtitleStreamIndex { get; set; } + + /// + /// Gets or sets the maximum transcoding audio channels. + /// + /// The maximum transcoding audio channels. + public int? TranscodingMaxAudioChannels { get; set; } + + /// + /// Gets or sets the global maximum audio channels. + /// + /// The global maximum audio channels. + public int? GlobalMaxAudioChannels { get; set; } + + /// + /// Gets or sets the audio bitrate. + /// + /// The audio bitrate. + public int? AudioBitrate { get; set; } + + /// + /// Gets or sets the audio sample rate. + /// + /// The audio sample rate. + public int? AudioSampleRate { get; set; } + + /// + /// Gets or sets the video bitrate. + /// + /// The video bitrate. + public int? VideoBitrate { get; set; } + + /// + /// Gets or sets the maximum output width. + /// + /// The output width. + public int? MaxWidth { get; set; } + + /// + /// Gets or sets the maximum output height. + /// + /// The maximum output height. + public int? MaxHeight { get; set; } + + /// + /// Gets or sets the maximum framerate. + /// + /// The maximum framerate. + public float? MaxFramerate { get; set; } + + /// + /// Gets or sets the device profile. + /// + /// The device profile. + public required DeviceProfile DeviceProfile { get; set; } + + /// + /// Gets or sets the device profile id. + /// + /// The device profile id. + public string? DeviceProfileId { get; set; } + + /// + /// Gets or sets the device id. + /// + /// The device id. + public string? DeviceId { get; set; } + + /// + /// Gets or sets the runtime ticks. + /// + /// The runtime ticks. + public long? RunTimeTicks { get; set; } + + /// + /// Gets or sets the transcode seek info. + /// + /// The transcode seek info. + public TranscodeSeekInfo TranscodeSeekInfo { get; set; } + + /// + /// Gets or sets a value indicating whether content length should be estimated. + /// + public bool EstimateContentLength { get; set; } + + /// + /// Gets or sets the media source info. + /// + /// The media source info. + public MediaSourceInfo? MediaSource { get; set; } + + /// + /// Gets or sets the subtitle codecs. + /// + /// The subtitle codecs. + public IReadOnlyList SubtitleCodecs { get; set; } + + /// + /// Gets or sets the subtitle delivery method. + /// + /// The subtitle delivery method. + public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; } + + /// + /// Gets or sets the subtitle format. + /// + /// The subtitle format. + public string? SubtitleFormat { get; set; } + + /// + /// Gets or sets the play session id. + /// + /// The play session id. + public string? PlaySessionId { get; set; } + + /// + /// Gets or sets the transcode reasons. + /// + /// The transcode reasons. + public TranscodeReason TranscodeReasons { get; set; } + + /// + /// Gets the stream options. + /// + /// The stream options. + public Dictionary StreamOptions { get; private set; } + + /// + /// Gets the media source id. + /// + /// The media source id. + public string? MediaSourceId => MediaSource?.Id; + + /// + /// Gets or sets a value indicating whether audio VBR encoding is enabled. + /// + public bool EnableAudioVbrEncoding { get; set; } + + /// + /// Gets a value indicating whether the stream is direct. + /// + public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay) + && PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay; + + /// + /// Gets the audio stream that will be used in the output stream. + /// + /// The audio stream. + public MediaStream? TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex); + + /// + /// Gets the video stream that will be used in the output stream. + /// + /// The video stream. + public MediaStream? TargetVideoStream => MediaSource?.VideoStream; + + /// + /// Gets the audio sample rate that will be in the output stream. + /// + /// The target audio sample rate. + public int? TargetAudioSampleRate + { + get { - AudioCodecs = Array.Empty(); - VideoCodecs = Array.Empty(); - SubtitleCodecs = Array.Empty(); - StreamOptions = new Dictionary(StringComparer.OrdinalIgnoreCase); + var stream = TargetAudioStream; + return AudioSampleRate.HasValue && !IsDirectStream + ? AudioSampleRate + : stream?.SampleRate; } + } - public Guid ItemId { get; set; } - - public PlayMethod PlayMethod { get; set; } - - public EncodingContext Context { get; set; } - - public DlnaProfileType MediaType { get; set; } - - public string? Container { get; set; } - - public MediaStreamProtocol SubProtocol { get; set; } - - public long StartPositionTicks { get; set; } - - public int? SegmentLength { get; set; } - - public int? MinSegments { get; set; } - - public bool BreakOnNonKeyFrames { get; set; } - - public bool RequireAvc { get; set; } - - public bool RequireNonAnamorphic { get; set; } - - public bool CopyTimestamps { get; set; } - - public bool EnableMpegtsM2TsMode { get; set; } - - public bool EnableSubtitlesInManifest { get; set; } - - public string[] AudioCodecs { get; set; } - - public string[] VideoCodecs { get; set; } - - public int? AudioStreamIndex { get; set; } - - public int? SubtitleStreamIndex { get; set; } - - public int? TranscodingMaxAudioChannels { get; set; } - - public int? GlobalMaxAudioChannels { get; set; } - - public int? AudioBitrate { get; set; } - - public int? AudioSampleRate { get; set; } - - public int? VideoBitrate { get; set; } - - public int? MaxWidth { get; set; } - - public int? MaxHeight { get; set; } - - public float? MaxFramerate { get; set; } - - public required DeviceProfile DeviceProfile { get; set; } - - public string? DeviceProfileId { get; set; } - - public string? DeviceId { get; set; } - - public long? RunTimeTicks { get; set; } - - public TranscodeSeekInfo TranscodeSeekInfo { get; set; } - - public bool EstimateContentLength { get; set; } - - public MediaSourceInfo? MediaSource { get; set; } - - public string[] SubtitleCodecs { get; set; } - - public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; } - - public string? SubtitleFormat { get; set; } - - public string? PlaySessionId { get; set; } - - public TranscodeReason TranscodeReasons { get; set; } - - public Dictionary StreamOptions { get; private set; } - - public string? MediaSourceId => MediaSource?.Id; - - public bool EnableAudioVbrEncoding { get; set; } - - public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay) - && PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay; - - /// - /// Gets the audio stream that will be used. - /// - public MediaStream? TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex); - - /// - /// Gets the video stream that will be used. - /// - public MediaStream? TargetVideoStream => MediaSource?.VideoStream; - - /// - /// Gets the audio sample rate that will be in the output stream. - /// - public int? TargetAudioSampleRate + /// + /// Gets the audio bit depth that will be in the output stream. + /// + /// The target bit depth. + public int? TargetAudioBitDepth + { + get { - get + if (IsDirectStream) { - var stream = TargetAudioStream; - return AudioSampleRate.HasValue && !IsDirectStream - ? AudioSampleRate - : stream?.SampleRate; - } - } - - /// - /// Gets the audio sample rate that will be in the output stream. - /// - public int? TargetAudioBitDepth - { - get - { - if (IsDirectStream) - { - return TargetAudioStream?.BitDepth; - } - - var targetAudioCodecs = TargetAudioCodec; - var audioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0]; - if (!string.IsNullOrEmpty(audioCodec)) - { - return GetTargetAudioBitDepth(audioCodec); - } - return TargetAudioStream?.BitDepth; } - } - /// - /// Gets the audio sample rate that will be in the output stream. - /// - public int? TargetVideoBitDepth - { - get + var targetAudioCodecs = TargetAudioCodec; + var audioCodec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0]; + if (!string.IsNullOrEmpty(audioCodec)) { - if (IsDirectStream) - { - return TargetVideoStream?.BitDepth; - } + return GetTargetAudioBitDepth(audioCodec); + } - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) - { - return GetTargetVideoBitDepth(videoCodec); - } + return TargetAudioStream?.BitDepth; + } + } + /// + /// Gets the video bit depth that will be in the output stream. + /// + /// The target video bit depth. + public int? TargetVideoBitDepth + { + get + { + if (IsDirectStream) + { return TargetVideoStream?.BitDepth; } - } - /// - /// Gets the target reference frames. - /// - /// The target reference frames. - public int? TargetRefFrames - { - get + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) { - if (IsDirectStream) - { - return TargetVideoStream?.RefFrames; - } + return GetTargetVideoBitDepth(videoCodec); + } - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) - { - return GetTargetRefFrames(videoCodec); - } + return TargetVideoStream?.BitDepth; + } + } + /// + /// Gets the target reference frames that will be in the output stream. + /// + /// The target reference frames. + public int? TargetRefFrames + { + get + { + if (IsDirectStream) + { return TargetVideoStream?.RefFrames; } - } - /// - /// Gets the audio sample rate that will be in the output stream. - /// - public float? TargetFramerate - { - get + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) { - var stream = TargetVideoStream; - return MaxFramerate.HasValue && !IsDirectStream - ? MaxFramerate - : stream?.ReferenceFrameRate; + return GetTargetRefFrames(videoCodec); } + + return TargetVideoStream?.RefFrames; } + } - /// - /// Gets the audio sample rate that will be in the output stream. - /// - public double? TargetVideoLevel + /// + /// Gets the target framerate that will be in the output stream. + /// + /// The target framerate. + public float? TargetFramerate + { + get { - get + var stream = TargetVideoStream; + return MaxFramerate.HasValue && !IsDirectStream + ? MaxFramerate + : stream?.ReferenceFrameRate; + } + } + + /// + /// Gets the target video level that will be in the output stream. + /// + /// The target video level. + public double? TargetVideoLevel + { + get + { + if (IsDirectStream) { - if (IsDirectStream) - { - return TargetVideoStream?.Level; - } - - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) - { - return GetTargetVideoLevel(videoCodec); - } - return TargetVideoStream?.Level; } - } - /// - /// Gets the audio sample rate that will be in the output stream. - /// - public int? TargetPacketLength - { - get + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) { - var stream = TargetVideoStream; - return !IsDirectStream - ? null - : stream?.PacketLength; + return GetTargetVideoLevel(videoCodec); } + + return TargetVideoStream?.Level; } + } - /// - /// Gets the audio sample rate that will be in the output stream. - /// - public string? TargetVideoProfile + /// + /// Gets the target packet length that will be in the output stream. + /// + /// The target packet length. + public int? TargetPacketLength + { + get { - get + var stream = TargetVideoStream; + return !IsDirectStream + ? null + : stream?.PacketLength; + } + } + + /// + /// Gets the target video profile that will be in the output stream. + /// + /// The target video profile. + public string? TargetVideoProfile + { + get + { + if (IsDirectStream) { - if (IsDirectStream) - { - return TargetVideoStream?.Profile; - } - - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) - { - return GetOption(videoCodec, "profile"); - } - return TargetVideoStream?.Profile; } - } - /// - /// Gets the target video range type that will be in the output stream. - /// - public VideoRangeType TargetVideoRangeType - { - get + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) { - if (IsDirectStream) - { - return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown; - } + return GetOption(videoCodec, "profile"); + } - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec) - && Enum.TryParse(GetOption(videoCodec, "rangetype"), true, out VideoRangeType videoRangeType)) - { - return videoRangeType; - } + return TargetVideoStream?.Profile; + } + } + /// + /// Gets the target video range type that will be in the output stream. + /// + /// The video range type. + public VideoRangeType TargetVideoRangeType + { + get + { + if (IsDirectStream) + { return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown; } - } - /// - /// Gets the target video codec tag. - /// - /// The target video codec tag. - public string? TargetVideoCodecTag - { - get + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec) + && Enum.TryParse(GetOption(videoCodec, "rangetype"), true, out VideoRangeType videoRangeType)) { - var stream = TargetVideoStream; - return !IsDirectStream - ? null - : stream?.CodecTag; + return videoRangeType; } + + return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown; } + } - /// - /// Gets the audio bitrate that will be in the output stream. - /// - public int? TargetAudioBitrate + /// + /// Gets the target video codec tag. + /// + /// The video codec tag. + public string? TargetVideoCodecTag + { + get { - get - { - var stream = TargetAudioStream; - return AudioBitrate.HasValue && !IsDirectStream - ? AudioBitrate - : stream?.BitRate; - } + var stream = TargetVideoStream; + return !IsDirectStream + ? null + : stream?.CodecTag; } + } - /// - /// Gets the audio channels that will be in the output stream. - /// - public int? TargetAudioChannels + /// + /// Gets the audio bitrate that will be in the output stream. + /// + /// The audio bitrate. + public int? TargetAudioBitrate + { + get { - get + var stream = TargetAudioStream; + return AudioBitrate.HasValue && !IsDirectStream + ? AudioBitrate + : stream?.BitRate; + } + } + + /// + /// Gets the amount of audio channels that will be in the output stream. + /// + /// The target audio channels. + public int? TargetAudioChannels + { + get + { + if (IsDirectStream) { - if (IsDirectStream) - { - return TargetAudioStream?.Channels; - } - - var targetAudioCodecs = TargetAudioCodec; - var codec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0]; - if (!string.IsNullOrEmpty(codec)) - { - return GetTargetRefFrames(codec); - } - return TargetAudioStream?.Channels; } - } - /// - /// Gets the audio codec that will be in the output stream. - /// - public string[] TargetAudioCodec - { - get + var targetAudioCodecs = TargetAudioCodec; + var codec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0]; + if (!string.IsNullOrEmpty(codec)) { - var stream = TargetAudioStream; + return GetTargetRefFrames(codec); + } - string? inputCodec = stream?.Codec; + return TargetAudioStream?.Channels; + } + } - if (IsDirectStream) + /// + /// Gets the audio codec that will be in the output stream. + /// + /// The audio codec. + public IReadOnlyList TargetAudioCodec + { + get + { + var stream = TargetAudioStream; + + string? inputCodec = stream?.Codec; + + if (IsDirectStream) + { + return string.IsNullOrEmpty(inputCodec) ? [] : [inputCodec]; + } + + foreach (string codec in AudioCodecs) + { + if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) { - return string.IsNullOrEmpty(inputCodec) ? Array.Empty() : new[] { inputCodec }; + return string.IsNullOrEmpty(codec) ? [] : [codec]; } + } - foreach (string codec in AudioCodecs) + return AudioCodecs; + } + } + + /// + /// Gets the video codec that will be in the output stream. + /// + /// The target video codec. + public IReadOnlyList TargetVideoCodec + { + get + { + var stream = TargetVideoStream; + + string? inputCodec = stream?.Codec; + + if (IsDirectStream) + { + return string.IsNullOrEmpty(inputCodec) ? [] : [inputCodec]; + } + + foreach (string codec in VideoCodecs) + { + if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) - { - return string.IsNullOrEmpty(codec) ? Array.Empty() : new[] { codec }; - } + return string.IsNullOrEmpty(codec) ? [] : [codec]; } - - return AudioCodecs; - } - } - - public string[] TargetVideoCodec - { - get - { - var stream = TargetVideoStream; - - string? inputCodec = stream?.Codec; - - if (IsDirectStream) - { - return string.IsNullOrEmpty(inputCodec) ? Array.Empty() : new[] { inputCodec }; - } - - foreach (string codec in VideoCodecs) - { - if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) - { - return string.IsNullOrEmpty(codec) ? Array.Empty() : new[] { codec }; - } - } - - return VideoCodecs; - } - } - - /// - /// Gets the audio channels that will be in the output stream. - /// - public long? TargetSize - { - get - { - if (IsDirectStream) - { - return MediaSource?.Size; - } - - if (RunTimeTicks.HasValue) - { - int? totalBitrate = TargetTotalBitrate; - - double totalSeconds = RunTimeTicks.Value; - // Convert to ms - totalSeconds /= 10000; - // Convert to seconds - totalSeconds /= 1000; - - return totalBitrate.HasValue ? - Convert.ToInt64(totalBitrate.Value * totalSeconds) : - null; - } - - return null; - } - } - - public int? TargetVideoBitrate - { - get - { - var stream = TargetVideoStream; - - return VideoBitrate.HasValue && !IsDirectStream - ? VideoBitrate - : stream?.BitRate; - } - } - - public TransportStreamTimestamp TargetTimestamp - { - get - { - var defaultValue = string.Equals(Container, "m2ts", StringComparison.OrdinalIgnoreCase) - ? TransportStreamTimestamp.Valid - : TransportStreamTimestamp.None; - - return !IsDirectStream - ? defaultValue - : MediaSource is null ? defaultValue : MediaSource.Timestamp ?? TransportStreamTimestamp.None; - } - } - - public int? TargetTotalBitrate => (TargetAudioBitrate ?? 0) + (TargetVideoBitrate ?? 0); - - public bool? IsTargetAnamorphic - { - get - { - if (IsDirectStream) - { - return TargetVideoStream?.IsAnamorphic; - } - - return false; - } - } - - public bool? IsTargetInterlaced - { - get - { - if (IsDirectStream) - { - return TargetVideoStream?.IsInterlaced; - } - - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) - { - if (string.Equals(GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - - return TargetVideoStream?.IsInterlaced; - } - } - - public bool? IsTargetAVC - { - get - { - if (IsDirectStream) - { - return TargetVideoStream?.IsAVC; - } - - return true; - } - } - - public int? TargetWidth - { - get - { - var videoStream = TargetVideoStream; - - if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue) - { - ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value); - - size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0); - - return size.Width; - } - - return MaxWidth; - } - } - - public int? TargetHeight - { - get - { - var videoStream = TargetVideoStream; - - if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue) - { - ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value); - - size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0); - - return size.Height; - } - - return MaxHeight; - } - } - - public int? TargetVideoStreamCount - { - get - { - if (IsDirectStream) - { - return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue); - } - - return GetMediaStreamCount(MediaStreamType.Video, 1); - } - } - - public int? TargetAudioStreamCount - { - get - { - if (IsDirectStream) - { - return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue); - } - - return GetMediaStreamCount(MediaStreamType.Audio, 1); - } - } - - public void SetOption(string? qualifier, string name, string value) - { - if (string.IsNullOrEmpty(qualifier)) - { - SetOption(name, value); - } - else - { - SetOption(qualifier + "-" + name, value); - } - } - - public void SetOption(string name, string value) - { - StreamOptions[name] = value; - } - - public string? GetOption(string? qualifier, string name) - { - var value = GetOption(qualifier + "-" + name); - - if (string.IsNullOrEmpty(value)) - { - value = GetOption(name); } - return value; + return VideoCodecs; } + } - public string? GetOption(string name) + /// + /// Gets the target size of the output stream. + /// + /// The target size. + public long? TargetSize + { + get { - if (StreamOptions.TryGetValue(name, out var value)) + if (IsDirectStream) { - return value; + return MediaSource?.Size; + } + + if (RunTimeTicks.HasValue) + { + int? totalBitrate = TargetTotalBitrate; + + double totalSeconds = RunTimeTicks.Value; + // Convert to ms + totalSeconds /= 10000; + // Convert to seconds + totalSeconds /= 1000; + + return totalBitrate.HasValue ? + Convert.ToInt64(totalBitrate.Value * totalSeconds) : + null; } return null; } + } - public string ToUrl(string baseUrl, string? accessToken) + /// + /// Gets the target video bitrate of the output stream. + /// + /// The video bitrate. + public int? TargetVideoBitrate + { + get { - ArgumentException.ThrowIfNullOrEmpty(baseUrl); + var stream = TargetVideoStream; - var list = new List(); - foreach (NameValuePair pair in BuildParams(this, accessToken)) + return VideoBitrate.HasValue && !IsDirectStream + ? VideoBitrate + : stream?.BitRate; + } + } + + /// + /// Gets the target timestamp of the output stream. + /// + /// The target timestamp. + public TransportStreamTimestamp TargetTimestamp + { + get + { + var defaultValue = string.Equals(Container, "m2ts", StringComparison.OrdinalIgnoreCase) + ? TransportStreamTimestamp.Valid + : TransportStreamTimestamp.None; + + return !IsDirectStream + ? defaultValue + : MediaSource is null ? defaultValue : MediaSource.Timestamp ?? TransportStreamTimestamp.None; + } + } + + /// + /// Gets the target total bitrate of the output stream. + /// + /// The target total bitrate. + public int? TargetTotalBitrate => (TargetAudioBitrate ?? 0) + (TargetVideoBitrate ?? 0); + + /// + /// Gets a value indicating whether the output stream is anamorphic. + /// + public bool? IsTargetAnamorphic + { + get + { + if (IsDirectStream) { - if (string.IsNullOrEmpty(pair.Value)) - { - continue; - } - - // Try to keep the url clean by omitting defaults - if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase) - && string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase) - && string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) - && string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal); - - list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue)); + return TargetVideoStream?.IsAnamorphic; } - string queryString = string.Join('&', list); + return false; + } + } - return GetUrl(baseUrl, queryString); + /// + /// Gets a value indicating whether the output stream is interlaced. + /// + public bool? IsTargetInterlaced + { + get + { + if (IsDirectStream) + { + return TargetVideoStream?.IsInterlaced; + } + + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) + { + if (string.Equals(GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + + return TargetVideoStream?.IsInterlaced; + } + } + + /// + /// Gets a value indicating whether the output stream is AVC. + /// + public bool? IsTargetAVC + { + get + { + if (IsDirectStream) + { + return TargetVideoStream?.IsAVC; + } + + return true; + } + } + + /// + /// Gets the target width of the output stream. + /// + /// The target width. + public int? TargetWidth + { + get + { + var videoStream = TargetVideoStream; + + if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue) + { + ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value); + + size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0); + + return size.Width; + } + + return MaxWidth; + } + } + + /// + /// Gets the target height of the output stream. + /// + /// The target height. + public int? TargetHeight + { + get + { + var videoStream = TargetVideoStream; + + if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue) + { + ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value); + + size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0); + + return size.Height; + } + + return MaxHeight; + } + } + + /// + /// Gets the target video stream count of the output stream. + /// + /// The target video stream count. + public int? TargetVideoStreamCount + { + get + { + if (IsDirectStream) + { + return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue); + } + + return GetMediaStreamCount(MediaStreamType.Video, 1); + } + } + + /// + /// Gets the target audio stream count of the output stream. + /// + /// The target audio stream count. + public int? TargetAudioStreamCount + { + get + { + if (IsDirectStream) + { + return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue); + } + + return GetMediaStreamCount(MediaStreamType.Audio, 1); + } + } + + /// + /// Sets a stream option. + /// + /// The qualifier. + /// The name. + /// The value. + public void SetOption(string? qualifier, string name, string value) + { + if (string.IsNullOrEmpty(qualifier)) + { + SetOption(name, value); + } + else + { + SetOption(qualifier + "-" + name, value); + } + } + + /// + /// Sets a stream option. + /// + /// The name. + /// The value. + public void SetOption(string name, string value) + { + StreamOptions[name] = value; + } + + /// + /// Gets a stream option. + /// + /// The qualifier. + /// The name. + /// The value. + public string? GetOption(string? qualifier, string name) + { + var value = GetOption(qualifier + "-" + name); + + if (string.IsNullOrEmpty(value)) + { + value = GetOption(name); } - private string GetUrl(string baseUrl, string queryString) + return value; + } + + /// + /// Gets a stream option. + /// + /// The name. + /// The value. + public string? GetOption(string name) + { + if (StreamOptions.TryGetValue(name, out var value)) { - ArgumentException.ThrowIfNullOrEmpty(baseUrl); + return value; + } - string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container; + return null; + } - baseUrl = baseUrl.TrimEnd('/'); + /// + /// Returns this output stream URL for this class. + /// + /// The base Url. + /// The access Token. + /// A querystring representation of this object. + public string ToUrl(string baseUrl, string? accessToken) + { + ArgumentException.ThrowIfNullOrEmpty(baseUrl); - if (MediaType == DlnaProfileType.Audio) + List list = []; + foreach (NameValuePair pair in BuildParams(this, accessToken)) + { + if (string.IsNullOrEmpty(pair.Value)) { - if (SubProtocol == MediaStreamProtocol.hls) - { - return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); - } - - return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); + continue; } + // Try to keep the url clean by omitting defaults + if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase) + && string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase) + && string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) + && string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal); + + list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue)); + } + + string queryString = string.Join('&', list); + + return GetUrl(baseUrl, queryString); + } + + private string GetUrl(string baseUrl, string queryString) + { + ArgumentException.ThrowIfNullOrEmpty(baseUrl); + + string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container; + + baseUrl = baseUrl.TrimEnd('/'); + + if (MediaType == DlnaProfileType.Audio) + { if (SubProtocol == MediaStreamProtocol.hls) { - return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); + return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); } - return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); + return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); } - private static IEnumerable BuildParams(StreamInfo item, string? accessToken) + if (SubProtocol == MediaStreamProtocol.hls) { - var list = new List(); - - string audioCodecs = item.AudioCodecs.Length == 0 ? - string.Empty : - string.Join(',', item.AudioCodecs); - - string videoCodecs = item.VideoCodecs.Length == 0 ? - string.Empty : - string.Join(',', item.VideoCodecs); - - list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty)); - list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty)); - list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty)); - list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - list.Add(new NameValuePair("VideoCodec", videoCodecs)); - list.Add(new NameValuePair("AudioCodec", audioCodecs)); - list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - - list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - - long startPositionTicks = item.StartPositionTicks; - - if (item.SubProtocol == MediaStreamProtocol.hls) - { - list.Add(new NameValuePair("StartTimeTicks", string.Empty)); - } - else - { - list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture))); - } - - list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty)); - list.Add(new NameValuePair("api_key", accessToken ?? string.Empty)); - - string? liveStreamId = item.MediaSource?.LiveStreamId; - list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty)); - - list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty)); - - if (!item.IsDirectStream) - { - if (item.RequireNonAnamorphic) - { - list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - - if (item.EnableSubtitlesInManifest) - { - list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - if (item.EnableMpegtsM2TsMode) - { - list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - if (item.EstimateContentLength) - { - list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto) - { - list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant())); - } - - if (item.CopyTimestamps) - { - list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - - list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty)); - - string subtitleCodecs = item.SubtitleCodecs.Length == 0 ? - string.Empty : - string.Join(",", item.SubtitleCodecs); - - list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty)); - - if (item.SubProtocol == MediaStreamProtocol.hls) - { - list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty)); - - if (item.SegmentLength.HasValue) - { - list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture))); - } - - if (item.MinSegments.HasValue) - { - list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture))); - } - - list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture))); - } - - foreach (var pair in item.StreamOptions) - { - if (string.IsNullOrEmpty(pair.Value)) - { - continue; - } - - // strip spaces to avoid having to encode h264 profile names - list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal))); - } - - if (!item.IsDirectStream) - { - list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString())); - } - - return list; + return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); } - public IEnumerable GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string? accessToken) + return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); + } + + private static List BuildParams(StreamInfo item, string? accessToken) + { + List list = []; + + string audioCodecs = item.AudioCodecs.Count == 0 ? + string.Empty : + string.Join(',', item.AudioCodecs); + + string videoCodecs = item.VideoCodecs.Count == 0 ? + string.Empty : + string.Join(',', item.VideoCodecs); + + list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty)); + list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty)); + list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty)); + list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + list.Add(new NameValuePair("VideoCodec", videoCodecs)); + list.Add(new NameValuePair("AudioCodec", audioCodecs)); + list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + + list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + + long startPositionTicks = item.StartPositionTicks; + + if (item.SubProtocol == MediaStreamProtocol.hls) { - return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken); + list.Add(new NameValuePair("StartTimeTicks", string.Empty)); + } + else + { + list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture))); } - public IEnumerable GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string? accessToken) + list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty)); + list.Add(new NameValuePair("api_key", accessToken ?? string.Empty)); + + string? liveStreamId = item.MediaSource?.LiveStreamId; + list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty)); + + list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty)); + + if (!item.IsDirectStream) { - if (MediaSource is null) + if (item.RequireNonAnamorphic) { - return Enumerable.Empty(); + list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } - var list = new List(); + list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - // HLS will preserve timestamps so we can just grab the full subtitle stream - long startPositionTicks = SubProtocol == MediaStreamProtocol.hls - ? 0 - : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0); - - // First add the selected track - if (SubtitleStreamIndex.HasValue) + if (item.EnableSubtitlesInManifest) { - foreach (var stream in MediaSource.MediaStreams) - { - if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value) - { - AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); - } - } + list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } - if (!includeSelectedTrackOnly) + if (item.EnableMpegtsM2TsMode) { - foreach (var stream in MediaSource.MediaStreams) - { - if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value)) - { - AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); - } - } + list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } - return list; + if (item.EstimateContentLength) + { + list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + } + + if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto) + { + list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant())); + } + + if (item.CopyTimestamps) + { + list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + } + + list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + + list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } - private void AddSubtitleProfiles(List list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string? accessToken, long startPositionTicks) + list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty)); + + string subtitleCodecs = item.SubtitleCodecs.Count == 0 ? + string.Empty : + string.Join(",", item.SubtitleCodecs); + + list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty)); + + if (item.SubProtocol == MediaStreamProtocol.hls) { - if (enableAllProfiles) + list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty)); + + if (item.SegmentLength.HasValue) { - foreach (var profile in DeviceProfile.SubtitleProfiles) + list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture))); + } + + if (item.MinSegments.HasValue) + { + list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture))); + } + + list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture))); + } + + foreach (var pair in item.StreamOptions) + { + if (string.IsNullOrEmpty(pair.Value)) + { + continue; + } + + // strip spaces to avoid having to encode h264 profile names + list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal))); + } + + if (!item.IsDirectStream) + { + list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString())); + } + + return list; + } + + /// + /// Gets the subtitle profiles. + /// + /// The transcoder support. + /// If only the selected track should be included. + /// The base URL. + /// The access token. + /// The of the profiles. + public IEnumerable GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string? accessToken) + { + return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken); + } + + /// + /// Gets the subtitle profiles. + /// + /// The transcoder support. + /// If only the selected track should be included. + /// If all profiles are enabled. + /// The base URL. + /// The access token. + /// The of the profiles. + public IEnumerable GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string? accessToken) + { + if (MediaSource is null) + { + return []; + } + + List list = []; + + // HLS will preserve timestamps so we can just grab the full subtitle stream + long startPositionTicks = SubProtocol == MediaStreamProtocol.hls + ? 0 + : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0); + + // First add the selected track + if (SubtitleStreamIndex.HasValue) + { + foreach (var stream in MediaSource.MediaStreams) + { + if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value) { - var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport); - if (info is not null) - { - list.Add(info); - } + AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); } } - else + } + + if (!includeSelectedTrackOnly) + { + foreach (var stream in MediaSource.MediaStreams) { - var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport); + if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value)) + { + AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); + } + } + } + + return list; + } + + private void AddSubtitleProfiles(List list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string? accessToken, long startPositionTicks) + { + if (enableAllProfiles) + { + foreach (var profile in DeviceProfile.SubtitleProfiles) + { + var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport); if (info is not null) { list.Add(info); } } } - - private SubtitleStreamInfo? GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string? accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport) + else { - if (MediaSource is null) + var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport); + if (info is not null) { - return null; + list.Add(info); } + } + } - var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol); - var info = new SubtitleStreamInfo - { - IsForced = stream.IsForced, - Language = stream.Language, - Name = stream.Language ?? "Unknown", - Format = subtitleProfile.Format, - Index = stream.Index, - DeliveryMethod = subtitleProfile.Method, - DisplayTitle = stream.DisplayTitle - }; + private SubtitleStreamInfo? GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string? accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport) + { + if (MediaSource is null) + { + return null; + } - if (info.DeliveryMethod == SubtitleDeliveryMethod.External) + var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol); + var info = new SubtitleStreamInfo + { + IsForced = stream.IsForced, + Language = stream.Language, + Name = stream.Language ?? "Unknown", + Format = subtitleProfile.Format, + Index = stream.Index, + DeliveryMethod = subtitleProfile.Method, + DisplayTitle = stream.DisplayTitle + }; + + if (info.DeliveryMethod == SubtitleDeliveryMethod.External) + { + if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal) { - if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal) + info.Url = string.Format( + CultureInfo.InvariantCulture, + "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}", + baseUrl, + ItemId, + MediaSourceId, + stream.Index.ToString(CultureInfo.InvariantCulture), + startPositionTicks.ToString(CultureInfo.InvariantCulture), + subtitleProfile.Format); + + if (!string.IsNullOrEmpty(accessToken)) { - info.Url = string.Format( - CultureInfo.InvariantCulture, - "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}", - baseUrl, - ItemId, - MediaSourceId, - stream.Index.ToString(CultureInfo.InvariantCulture), - startPositionTicks.ToString(CultureInfo.InvariantCulture), - subtitleProfile.Format); - - if (!string.IsNullOrEmpty(accessToken)) - { - info.Url += "?api_key=" + accessToken; - } - - info.IsExternalUrl = false; + info.Url += "?api_key=" + accessToken; } - else - { - info.Url = stream.Path; - info.IsExternalUrl = true; - } - } - return info; + info.IsExternalUrl = false; + } + else + { + info.Url = stream.Path; + info.IsExternalUrl = true; + } } - public int? GetTargetVideoBitDepth(string? codec) + return info; + } + + /// + /// Gets the target video bit depth. + /// + /// The codec. + /// The target video bit depth. + public int? GetTargetVideoBitDepth(string? codec) + { + var value = GetOption(codec, "videobitdepth"); + + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { - var value = GetOption(codec, "videobitdepth"); - - if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) - { - return result; - } - - return null; + return result; } - public int? GetTargetAudioBitDepth(string? codec) + return null; + } + + /// + /// Gets the target audio bit depth. + /// + /// The codec. + /// The target audio bit depth. + public int? GetTargetAudioBitDepth(string? codec) + { + var value = GetOption(codec, "audiobitdepth"); + + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { - var value = GetOption(codec, "audiobitdepth"); - - if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) - { - return result; - } - - return null; + return result; } - public double? GetTargetVideoLevel(string? codec) + return null; + } + + /// + /// Gets the target video level. + /// + /// The codec. + /// The target video level. + public double? GetTargetVideoLevel(string? codec) + { + var value = GetOption(codec, "level"); + + if (double.TryParse(value, CultureInfo.InvariantCulture, out var result)) { - var value = GetOption(codec, "level"); - - if (double.TryParse(value, CultureInfo.InvariantCulture, out var result)) - { - return result; - } - - return null; + return result; } - public int? GetTargetRefFrames(string? codec) + return null; + } + + /// + /// Gets the target reference frames. + /// + /// The codec. + /// The target reference frames. + public int? GetTargetRefFrames(string? codec) + { + var value = GetOption(codec, "maxrefframes"); + + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { - var value = GetOption(codec, "maxrefframes"); - - if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) - { - return result; - } - - return null; + return result; } - public int? GetTargetAudioChannels(string? codec) + return null; + } + + /// + /// Gets the target audio channels. + /// + /// The codec. + /// The target audio channels. + public int? GetTargetAudioChannels(string? codec) + { + var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels; + + var value = GetOption(codec, "audiochannels"); + if (string.IsNullOrEmpty(value)) { - var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels; - - var value = GetOption(codec, "audiochannels"); - if (string.IsNullOrEmpty(value)) - { - return defaultValue; - } - - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) - { - return Math.Min(result, defaultValue ?? result); - } - return defaultValue; } - private int? GetMediaStreamCount(MediaStreamType type, int limit) + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { - var count = MediaSource?.GetStreamCount(type); - - if (count.HasValue) - { - count = Math.Min(count.Value, limit); - } - - return count; + return Math.Min(result, defaultValue ?? result); } + + return defaultValue; + } + + /// + /// Gets the media stream count. + /// + /// The type. + /// The limit. + /// The media stream count. + private int? GetMediaStreamCount(MediaStreamType type, int limit) + { + var count = MediaSource?.GetStreamCount(type); + + if (count.HasValue) + { + count = Math.Min(count.Value, limit); + } + + return count; } } diff --git a/MediaBrowser.Model/Dlna/SubtitleProfile.cs b/MediaBrowser.Model/Dlna/SubtitleProfile.cs index 9ebde25ffe..1879f2dd23 100644 --- a/MediaBrowser.Model/Dlna/SubtitleProfile.cs +++ b/MediaBrowser.Model/Dlna/SubtitleProfile.cs @@ -1,48 +1,62 @@ #nullable disable -#pragma warning disable CS1591 -using System; using System.Xml.Serialization; -using Jellyfin.Extensions; +using MediaBrowser.Model.Extensions; -namespace MediaBrowser.Model.Dlna +namespace MediaBrowser.Model.Dlna; + +/// +/// A class for subtitle profile information. +/// +public class SubtitleProfile { - public class SubtitleProfile + /// + /// Gets or sets the format. + /// + [XmlAttribute("format")] + public string Format { get; set; } + + /// + /// Gets or sets the delivery method. + /// + [XmlAttribute("method")] + public SubtitleDeliveryMethod Method { get; set; } + + /// + /// Gets or sets the DIDL mode. + /// + [XmlAttribute("didlMode")] + public string DidlMode { get; set; } + + /// + /// Gets or sets the language. + /// + [XmlAttribute("language")] + public string Language { get; set; } + + /// + /// Gets or sets the container. + /// + [XmlAttribute("container")] + public string Container { get; set; } + + /// + /// Checks if a language is supported. + /// + /// The language to check for support. + /// true if supported. + public bool SupportsLanguage(string subLanguage) { - [XmlAttribute("format")] - public string Format { get; set; } - - [XmlAttribute("method")] - public SubtitleDeliveryMethod Method { get; set; } - - [XmlAttribute("didlMode")] - public string DidlMode { get; set; } - - [XmlAttribute("language")] - public string Language { get; set; } - - [XmlAttribute("container")] - public string Container { get; set; } - - public string[] GetLanguages() + if (string.IsNullOrEmpty(Language)) { - return ContainerProfile.SplitValue(Language); + return true; } - public bool SupportsLanguage(string subLanguage) + if (string.IsNullOrEmpty(subLanguage)) { - if (string.IsNullOrEmpty(Language)) - { - return true; - } - - if (string.IsNullOrEmpty(subLanguage)) - { - subLanguage = "und"; - } - - var languages = GetLanguages(); - return languages.Length == 0 || languages.Contains(subLanguage, StringComparison.OrdinalIgnoreCase); + subLanguage = "und"; } + + return ContainerHelper.ContainsContainer(Language, subLanguage); } } diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs index a556799deb..5a9fa22ae4 100644 --- a/MediaBrowser.Model/Dlna/TranscodingProfile.cs +++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs @@ -1,82 +1,130 @@ -#pragma warning disable CS1591 - -using System; using System.ComponentModel; using System.Xml.Serialization; using Jellyfin.Data.Enums; -namespace MediaBrowser.Model.Dlna +namespace MediaBrowser.Model.Dlna; + +/// +/// A class for transcoding profile information. +/// +public class TranscodingProfile { - public class TranscodingProfile + /// + /// Initializes a new instance of the class. + /// + public TranscodingProfile() { - public TranscodingProfile() - { - Conditions = Array.Empty(); - } - - [XmlAttribute("container")] - public string Container { get; set; } = string.Empty; - - [XmlAttribute("type")] - public DlnaProfileType Type { get; set; } - - [XmlAttribute("videoCodec")] - public string VideoCodec { get; set; } = string.Empty; - - [XmlAttribute("audioCodec")] - public string AudioCodec { get; set; } = string.Empty; - - [XmlAttribute("protocol")] - public MediaStreamProtocol Protocol { get; set; } = MediaStreamProtocol.http; - - [DefaultValue(false)] - [XmlAttribute("estimateContentLength")] - public bool EstimateContentLength { get; set; } - - [DefaultValue(false)] - [XmlAttribute("enableMpegtsM2TsMode")] - public bool EnableMpegtsM2TsMode { get; set; } - - [DefaultValue(TranscodeSeekInfo.Auto)] - [XmlAttribute("transcodeSeekInfo")] - public TranscodeSeekInfo TranscodeSeekInfo { get; set; } - - [DefaultValue(false)] - [XmlAttribute("copyTimestamps")] - public bool CopyTimestamps { get; set; } - - [DefaultValue(EncodingContext.Streaming)] - [XmlAttribute("context")] - public EncodingContext Context { get; set; } - - [DefaultValue(false)] - [XmlAttribute("enableSubtitlesInManifest")] - public bool EnableSubtitlesInManifest { get; set; } - - [XmlAttribute("maxAudioChannels")] - public string? MaxAudioChannels { get; set; } - - [DefaultValue(0)] - [XmlAttribute("minSegments")] - public int MinSegments { get; set; } - - [DefaultValue(0)] - [XmlAttribute("segmentLength")] - public int SegmentLength { get; set; } - - [DefaultValue(false)] - [XmlAttribute("breakOnNonKeyFrames")] - public bool BreakOnNonKeyFrames { get; set; } - - public ProfileCondition[] Conditions { get; set; } - - [DefaultValue(true)] - [XmlAttribute("enableAudioVbrEncoding")] - public bool EnableAudioVbrEncoding { get; set; } = true; - - public string[] GetAudioCodecs() - { - return ContainerProfile.SplitValue(AudioCodec); - } + Conditions = []; } + + /// + /// Gets or sets the container. + /// + [XmlAttribute("container")] + public string Container { get; set; } = string.Empty; + + /// + /// Gets or sets the DLNA profile type. + /// + [XmlAttribute("type")] + public DlnaProfileType Type { get; set; } + + /// + /// Gets or sets the video codec. + /// + [XmlAttribute("videoCodec")] + public string VideoCodec { get; set; } = string.Empty; + + /// + /// Gets or sets the audio codec. + /// + [XmlAttribute("audioCodec")] + public string AudioCodec { get; set; } = string.Empty; + + /// + /// Gets or sets the protocol. + /// + [XmlAttribute("protocol")] + public MediaStreamProtocol Protocol { get; set; } = MediaStreamProtocol.http; + + /// + /// Gets or sets a value indicating whether the content length should be estimated. + /// + [DefaultValue(false)] + [XmlAttribute("estimateContentLength")] + public bool EstimateContentLength { get; set; } + + /// + /// Gets or sets a value indicating whether M2TS mode is enabled. + /// + [DefaultValue(false)] + [XmlAttribute("enableMpegtsM2TsMode")] + public bool EnableMpegtsM2TsMode { get; set; } + + /// + /// Gets or sets the transcoding seek info mode. + /// + [DefaultValue(TranscodeSeekInfo.Auto)] + [XmlAttribute("transcodeSeekInfo")] + public TranscodeSeekInfo TranscodeSeekInfo { get; set; } + + /// + /// Gets or sets a value indicating whether timestamps should be copied. + /// + [DefaultValue(false)] + [XmlAttribute("copyTimestamps")] + public bool CopyTimestamps { get; set; } + + /// + /// Gets or sets the encoding context. + /// + [DefaultValue(EncodingContext.Streaming)] + [XmlAttribute("context")] + public EncodingContext Context { get; set; } + + /// + /// Gets or sets a value indicating whether subtitles are allowed in the manifest. + /// + [DefaultValue(false)] + [XmlAttribute("enableSubtitlesInManifest")] + public bool EnableSubtitlesInManifest { get; set; } + + /// + /// Gets or sets the maximum audio channels. + /// + [XmlAttribute("maxAudioChannels")] + public string? MaxAudioChannels { get; set; } + + /// + /// Gets or sets the minimum amount of segments. + /// + [DefaultValue(0)] + [XmlAttribute("minSegments")] + public int MinSegments { get; set; } + + /// + /// Gets or sets the segment length. + /// + [DefaultValue(0)] + [XmlAttribute("segmentLength")] + public int SegmentLength { get; set; } + + /// + /// Gets or sets a value indicating whether breaking the video stream on non-keyframes is supported. + /// + [DefaultValue(false)] + [XmlAttribute("breakOnNonKeyFrames")] + public bool BreakOnNonKeyFrames { get; set; } + + /// + /// Gets or sets the profile conditions. + /// + public ProfileCondition[] Conditions { get; set; } + + /// + /// Gets or sets a value indicating whether variable bitrate encoding is supported. + /// + [DefaultValue(true)] + [XmlAttribute("enableAudioVbrEncoding")] + public bool EnableAudioVbrEncoding { get; set; } = true; } diff --git a/MediaBrowser.Model/Extensions/ContainerHelper.cs b/MediaBrowser.Model/Extensions/ContainerHelper.cs new file mode 100644 index 0000000000..4b75657ff8 --- /dev/null +++ b/MediaBrowser.Model/Extensions/ContainerHelper.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Extensions; + +namespace MediaBrowser.Model.Extensions; + +/// +/// Defines the class. +/// +public static class ContainerHelper +{ + /// + /// Compares two containers, returning true if an item in exists + /// in . + /// + /// The comma-delimited string being searched. + /// If the parameter begins with the - character, the operation is reversed. + /// The comma-delimited string being matched. + /// The result of the operation. + public static bool ContainsContainer(string? profileContainers, string? inputContainer) + { + var isNegativeList = false; + if (profileContainers != null && profileContainers.StartsWith('-')) + { + isNegativeList = true; + profileContainers = profileContainers[1..]; + } + + return ContainsContainer(profileContainers, isNegativeList, inputContainer); + } + + /// + /// Compares two containers, returning true if an item in exists + /// in . + /// + /// The comma-delimited string being searched. + /// If the parameter begins with the - character, the operation is reversed. + /// The comma-delimited string being matched. + /// The result of the operation. + public static bool ContainsContainer(string? profileContainers, ReadOnlySpan inputContainer) + { + var isNegativeList = false; + if (profileContainers != null && profileContainers.StartsWith('-')) + { + isNegativeList = true; + profileContainers = profileContainers[1..]; + } + + return ContainsContainer(profileContainers, isNegativeList, inputContainer); + } + + /// + /// Compares two containers, returning if an item in + /// does not exist in . + /// + /// The comma-delimited string being searched. + /// The boolean result to return if a match is not found. + /// The comma-delimited string being matched. + /// The result of the operation. + public static bool ContainsContainer(string? profileContainers, bool isNegativeList, string? inputContainer) + { + if (string.IsNullOrEmpty(inputContainer)) + { + return isNegativeList; + } + + return ContainsContainer(profileContainers, isNegativeList, inputContainer.AsSpan()); + } + + /// + /// Compares two containers, returning if an item in + /// does not exist in . + /// + /// The comma-delimited string being searched. + /// The boolean result to return if a match is not found. + /// The comma-delimited string being matched. + /// The result of the operation. + public static bool ContainsContainer(string? profileContainers, bool isNegativeList, ReadOnlySpan inputContainer) + { + if (string.IsNullOrEmpty(profileContainers)) + { + // Empty profiles always support all containers/codecs. + return true; + } + + var allInputContainers = inputContainer.Split(','); + var allProfileContainers = profileContainers.SpanSplit(','); + foreach (var container in allInputContainers) + { + if (!container.IsEmpty) + { + foreach (var profile in allProfileContainers) + { + if (container.Equals(profile, StringComparison.OrdinalIgnoreCase)) + { + return !isNegativeList; + } + } + } + } + + return isNegativeList; + } + + /// + /// Compares two containers, returning if an item in + /// does not exist in . + /// + /// The profile containers being matched searched. + /// The boolean result to return if a match is not found. + /// The comma-delimited string being matched. + /// The result of the operation. + public static bool ContainsContainer(IReadOnlyList? profileContainers, bool isNegativeList, string inputContainer) + { + if (profileContainers is null) + { + // Empty profiles always support all containers/codecs. + return true; + } + + var allInputContainers = inputContainer.Split(','); + foreach (var container in allInputContainers) + { + foreach (var profile in profileContainers) + { + if (string.Equals(profile, container, StringComparison.OrdinalIgnoreCase)) + { + return !isNegativeList; + } + } + } + + return isNegativeList; + } + + /// + /// Splits and input string. + /// + /// The input string. + /// The result of the operation. + public static string[] Split(string? input) + { + return input?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? []; + } +} diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 51ac558b86..80bb1a514c 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -316,7 +316,7 @@ namespace MediaBrowser.Providers.MediaInfo genres = genres.SelectMany(g => SplitWithCustomDelimiter(g, libraryOptions.CustomTagDelimiters, libraryOptions.DelimiterWhitelist)).ToArray(); } - audio.Genres = options.ReplaceAllMetadata || audio.Genres == null || audio.Genres.Length == 0 + audio.Genres = options.ReplaceAllMetadata || audio.Genres is null || audio.Genres.Length == 0 ? genres : audio.Genres; } diff --git a/tests/Jellyfin.Model.Tests/Dlna/ContainerHelperTests.cs b/tests/Jellyfin.Model.Tests/Dlna/ContainerHelperTests.cs new file mode 100644 index 0000000000..68f8d94c72 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Dlna/ContainerHelperTests.cs @@ -0,0 +1,54 @@ +using System; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Extensions; +using Xunit; + +namespace Jellyfin.Model.Tests.Dlna; + +public class ContainerHelperTests +{ + private readonly ContainerProfile _emptyContainerProfile = new ContainerProfile(); + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("mp4")] + public void ContainsContainer_EmptyContainerProfile_ReturnsTrue(string? containers) + { + Assert.True(_emptyContainerProfile.ContainsContainer(containers)); + } + + [Theory] + [InlineData("mp3,mpeg", "mp3")] + [InlineData("mp3,mpeg,avi", "mp3,avi")] + [InlineData("-mp3,mpeg", "avi")] + [InlineData("-mp3,mpeg,avi", "mp4,jpg")] + public void ContainsContainer_InList_ReturnsTrue(string container, string? extension) + { + Assert.True(ContainerHelper.ContainsContainer(container, extension)); + } + + [Theory] + [InlineData("mp3,mpeg", "avi")] + [InlineData("mp3,mpeg,avi", "mp4,jpg")] + [InlineData("mp3,mpeg", null)] + [InlineData("mp3,mpeg", "")] + [InlineData("-mp3,mpeg", "mp3")] + [InlineData("-mp3,mpeg,avi", "mpeg,avi")] + [InlineData(",mp3,", ",avi,")] // Empty values should be discarded + [InlineData("-,mp3,", ",mp3,")] // Empty values should be discarded + public void ContainsContainer_NotInList_ReturnsFalse(string container, string? extension) + { + Assert.False(ContainerHelper.ContainsContainer(container, extension)); + } + + [Theory] + [InlineData("mp3,mpeg", "mp3")] + [InlineData("mp3,mpeg,avi", "mp3,avi")] + [InlineData("-mp3,mpeg", "avi")] + [InlineData("-mp3,mpeg,avi", "mp4,jpg")] + public void ContainsContainer_InList_ReturnsTrue_SpanVersion(string container, string? extension) + { + Assert.True(ContainerHelper.ContainsContainer(container, extension.AsSpan())); + } +} diff --git a/tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs b/tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs deleted file mode 100644 index cca056c280..0000000000 --- a/tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs +++ /dev/null @@ -1,19 +0,0 @@ -using MediaBrowser.Model.Dlna; -using Xunit; - -namespace Jellyfin.Model.Tests.Dlna -{ - public class ContainerProfileTests - { - private readonly ContainerProfile _emptyContainerProfile = new ContainerProfile(); - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("mp4")] - public void ContainsContainer_EmptyContainerProfile_True(string? containers) - { - Assert.True(_emptyContainerProfile.ContainsContainer(containers)); - } - } -} diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 7b4bb05ff1..bd2143f252 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -389,18 +389,23 @@ namespace Jellyfin.Model.Tests if (playMethod == PlayMethod.DirectPlay) { // Check expected container - var containers = ContainerProfile.SplitValue(mediaSource.Container); + var containers = mediaSource.Container.Split(','); + Assert.Contains(uri.Extension, containers); // TODO: Test transcode too - // Assert.Contains(uri.Extension, containers); // Check expected video codec (1) - Assert.Contains(targetVideoStream?.Codec, streamInfo.TargetVideoCodec); - Assert.Single(streamInfo.TargetVideoCodec); + if (targetVideoStream?.Codec is not null) + { + Assert.Contains(targetVideoStream?.Codec, streamInfo.TargetVideoCodec); + Assert.Single(streamInfo.TargetVideoCodec); + } - // Check expected audio codecs (1) - Assert.Contains(targetAudioStream?.Codec, streamInfo.TargetAudioCodec); - Assert.Single(streamInfo.TargetAudioCodec); - // Assert.Single(val.AudioCodecs); + if (targetAudioStream?.Codec is not null) + { + // Check expected audio codecs (1) + Assert.Contains(targetAudioStream?.Codec, streamInfo.TargetAudioCodec); + Assert.Single(streamInfo.TargetAudioCodec); + } if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal)) { From 5a5da33f44b933215c95947c479ded1cdbadbcd9 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 17 Sep 2024 23:34:12 +0200 Subject: [PATCH 077/159] Apply review suggestions --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 7 ++--- .../Extensions/ContainerHelper.cs | 4 +-- .../Dlna/ContainerHelperTests.cs | 29 +++++++++++++++++++ 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 6fc7f796de..bf122dcc7f 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -407,10 +407,9 @@ namespace MediaBrowser.Model.Dlna continue; } - var formatStr = format.ToString(); - if (directPlayProfile.SupportsContainer(formatStr)) + if (directPlayProfile.SupportsContainer(format)) { - return formatStr; + return format; } } } @@ -1317,6 +1316,7 @@ namespace MediaBrowser.Model.Dlna } var containerSupported = false; + TranscodeReason[] rankings = [TranscodeReason.VideoCodecNotSupported, VideoCodecReasons, TranscodeReason.AudioCodecNotSupported, AudioCodecReasons, ContainerReasons]; // Check DirectPlay profiles to see if it can be direct played var analyzedProfiles = profile.DirectPlayProfiles @@ -1382,7 +1382,6 @@ namespace MediaBrowser.Model.Dlna playMethod = PlayMethod.DirectStream; } - TranscodeReason[] rankings = [TranscodeReason.VideoCodecNotSupported, VideoCodecReasons, TranscodeReason.AudioCodecNotSupported, AudioCodecReasons, ContainerReasons]; var ranked = GetRank(ref failureReasons, rankings); return (Result: (Profile: directPlayProfile, PlayMethod: playMethod, AudioStreamIndex: selectedAudioStream?.Index, TranscodeReason: failureReasons), Order: order, Rank: ranked); diff --git a/MediaBrowser.Model/Extensions/ContainerHelper.cs b/MediaBrowser.Model/Extensions/ContainerHelper.cs index 4b75657ff8..c86328ba68 100644 --- a/MediaBrowser.Model/Extensions/ContainerHelper.cs +++ b/MediaBrowser.Model/Extensions/ContainerHelper.cs @@ -91,7 +91,7 @@ public static class ContainerHelper { foreach (var profile in allProfileContainers) { - if (container.Equals(profile, StringComparison.OrdinalIgnoreCase)) + if (!profile.IsEmpty && container.Equals(profile, StringComparison.OrdinalIgnoreCase)) { return !isNegativeList; } @@ -118,7 +118,7 @@ public static class ContainerHelper return true; } - var allInputContainers = inputContainer.Split(','); + var allInputContainers = Split(inputContainer); foreach (var container in allInputContainers) { foreach (var profile in profileContainers) diff --git a/tests/Jellyfin.Model.Tests/Dlna/ContainerHelperTests.cs b/tests/Jellyfin.Model.Tests/Dlna/ContainerHelperTests.cs index 68f8d94c72..1ad4bed567 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/ContainerHelperTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/ContainerHelperTests.cs @@ -40,6 +40,11 @@ public class ContainerHelperTests public void ContainsContainer_NotInList_ReturnsFalse(string container, string? extension) { Assert.False(ContainerHelper.ContainsContainer(container, extension)); + + if (extension is not null) + { + Assert.False(ContainerHelper.ContainsContainer(container, extension.AsSpan())); + } } [Theory] @@ -51,4 +56,28 @@ public class ContainerHelperTests { Assert.True(ContainerHelper.ContainsContainer(container, extension.AsSpan())); } + + [Theory] + [InlineData(new string[] { "mp3", "mpeg" }, false, "mpeg")] + [InlineData(new string[] { "mp3", "mpeg", "avi" }, false, "avi")] + [InlineData(new string[] { "mp3", "", "avi" }, false, "mp3")] + [InlineData(new string[] { "mp3", "mpeg" }, true, "avi")] + [InlineData(new string[] { "mp3", "mpeg", "avi" }, true, "mkv")] + [InlineData(new string[] { "mp3", "", "avi" }, true, "")] + public void ContainsContainer_ThreeArgs_InList_ReturnsTrue(string[] containers, bool isNegativeList, string inputContainer) + { + Assert.True(ContainerHelper.ContainsContainer(containers, isNegativeList, inputContainer)); + } + + [Theory] + [InlineData(new string[] { "mp3", "mpeg" }, false, "avi")] + [InlineData(new string[] { "mp3", "mpeg", "avi" }, false, "mkv")] + [InlineData(new string[] { "mp3", "", "avi" }, false, "")] + [InlineData(new string[] { "mp3", "mpeg" }, true, "mpeg")] + [InlineData(new string[] { "mp3", "mpeg", "avi" }, true, "mp3")] + [InlineData(new string[] { "mp3", "", "avi" }, true, "avi")] + public void ContainsContainer_ThreeArgs_InList_ReturnsFalse(string[] containers, bool isNegativeList, string inputContainer) + { + Assert.False(ContainerHelper.ContainsContainer(containers, isNegativeList, inputContainer)); + } } From 0f9a8d8ee113d2e7aaae8a0687938fba9245229b Mon Sep 17 00:00:00 2001 From: Chris Lee Date: Tue, 17 Sep 2024 07:40:26 +0000 Subject: [PATCH 078/159] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)=20Translation:=20Jellyfin/Jellyfin=20Translat?= =?UTF-8?q?e-URL:=20https://translate.jellyfin.org/projects/jellyfin/jelly?= =?UTF-8?q?fin-core/nb=5FNO/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Emby.Server.Implementations/Localization/Core/nb.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index 6d644976d2..b90d06c7bb 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -129,9 +129,12 @@ "TaskRefreshTrickplayImagesDescription": "Oppretter trickplay-forhåndsvisninger for videoer i aktiverte biblioteker.", "TaskCleanCollectionsAndPlaylists": "Rydd kolleksjoner og spillelister", "TaskAudioNormalization": "Lyd Normalisering", - "TaskAudioNormalizationDescription": "Skan filer for lyd normaliserende data", - "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra kolleksjoner og spillelister som ikke lengere finnes", + "TaskAudioNormalizationDescription": "Skan filer for lyd normaliserende data.", + "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra kolleksjoner og spillelister som ikke lengere finnes.", "TaskDownloadMissingLyrics": "Last ned manglende tekster", "TaskDownloadMissingLyricsDescription": "Last ned sangtekster", - "TaskExtractMediaSegments": "Skann mediasegment" + "TaskExtractMediaSegments": "Skann mediasegment", + "TaskMoveTrickplayImages": "Migrer bildeplassering for Trickplay", + "TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til bibliotekseinstillingene.", + "TaskExtractMediaSegmentsDescription": "Trekker ut eller henter mediasegmenter fra plugins som støtter MediaSegment." } From 901573473d0f1b2e6b852ba6f92110b9d7bb2c0f Mon Sep 17 00:00:00 2001 From: gnattu Date: Wed, 18 Sep 2024 21:22:33 +0800 Subject: [PATCH 079/159] Sort by version name before resolution sorting (#12621) --- Emby.Naming/Video/VideoListResolver.cs | 4 +- .../Video/MultiVersionTests.cs | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 51f29cf088..12bc22a6ac 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -141,7 +141,9 @@ namespace Emby.Naming.Video { if (group.Key) { - videos.InsertRange(0, group.OrderByDescending(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator())); + videos.InsertRange(0, group + .OrderByDescending(x => ResolutionRegex().Match(x.Files[0].FileNameWithoutExtension.ToString()).Value, new AlphanumericComparator()) + .ThenBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator())); } else { diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs index 183ec89848..3005a4416c 100644 --- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs @@ -356,6 +356,45 @@ namespace Jellyfin.Naming.Tests.Video Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv", result[0].AlternateVersions[4].Path); } + [Fact] + public void TestMultiVersion13() + { + var files = new[] + { + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv", + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv", + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv", + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p Directors Cut.mkv", + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p Remux.mkv", + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p Theatrical Release.mkv", + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv", + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p Remux.mkv", + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p Directors Cut.mkv", + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p High Bitrate.mkv", + "/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv", + }; + + var result = VideoListResolver.Resolve( + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), + _namingOptions).ToList(); + + Assert.Single(result); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv", result[0].Files[0].Path); + Assert.Equal(11, result[0].AlternateVersions.Count); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv", result[0].AlternateVersions[0].Path); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p Remux.mkv", result[0].AlternateVersions[1].Path); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", result[0].AlternateVersions[2].Path); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p Directors Cut.mkv", result[0].AlternateVersions[3].Path); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p High Bitrate.mkv", result[0].AlternateVersions[4].Path); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p Remux.mkv", result[0].AlternateVersions[5].Path); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p Theatrical Release.mkv", result[0].AlternateVersions[6].Path); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv", result[0].AlternateVersions[7].Path); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p Directors Cut.mkv", result[0].AlternateVersions[8].Path); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv", result[0].AlternateVersions[9].Path); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv", result[0].AlternateVersions[10].Path); + } + [Fact] public void Resolve_GivenFolderNameWithBracketsAndHyphens_GroupsBasedOnFolderName() { From 97d2f778f8490d12d4171acfcfd873a36587da8d Mon Sep 17 00:00:00 2001 From: gnattu Date: Wed, 18 Sep 2024 21:22:53 +0800 Subject: [PATCH 080/159] Only sort item by width when they have the same path (#12626) --- MediaBrowser.Controller/Entities/BaseItem.cs | 7 +-- .../Entities/MediaSourceWidthComparator.cs | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 MediaBrowser.Controller/Entities/MediaSourceWidthComparator.cs diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 05a7b7896f..414488853f 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1087,12 +1087,7 @@ namespace MediaBrowser.Controller.Entities return 1; }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0) - .ThenByDescending(i => - { - var stream = i.VideoStream; - - return stream is null || stream.Width is null ? 0 : stream.Width.Value; - }) + .ThenByDescending(i => i, new MediaSourceWidthComparator()) .ToList(); } diff --git a/MediaBrowser.Controller/Entities/MediaSourceWidthComparator.cs b/MediaBrowser.Controller/Entities/MediaSourceWidthComparator.cs new file mode 100644 index 0000000000..0224577a4c --- /dev/null +++ b/MediaBrowser.Controller/Entities/MediaSourceWidthComparator.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Intrinsics.X86; +using MediaBrowser.Model.Dto; + +namespace MediaBrowser.Controller.Entities; + +/// +/// Compare MediaSource of the same file by Video width . +/// +public class MediaSourceWidthComparator : IComparer +{ + /// + public int Compare(MediaSourceInfo? x, MediaSourceInfo? y) + { + if (x is null && y is null) + { + return 0; + } + + if (x is null) + { + return -1; + } + + if (y is null) + { + return 1; + } + + if (string.Equals(x.Path, y.Path, StringComparison.OrdinalIgnoreCase)) + { + if (x.VideoStream is null && y.VideoStream is null) + { + return 0; + } + + if (x.VideoStream is null) + { + return -1; + } + + if (y.VideoStream is null) + { + return 1; + } + + var xWidth = x.VideoStream.Width ?? 0; + var yWidth = y.VideoStream.Width ?? 0; + + return xWidth - yWidth; + } + + return 0; + } +} From 569a41fc2a518672684b28a106241ecd8c9ceb67 Mon Sep 17 00:00:00 2001 From: gnattu Date: Wed, 18 Sep 2024 21:25:28 +0800 Subject: [PATCH 081/159] Don't expose hwaccel type for non-admin (#12663) --- Jellyfin.Api/Controllers/SessionController.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 60de66ab00..942bdeb9e8 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -16,6 +16,7 @@ using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -66,6 +67,7 @@ public class SessionController : BaseJellyfinApiController [FromQuery] int? activeWithinSeconds) { var result = _sessionManager.Sessions; + var isRequestingFromAdmin = User.IsInRole(UserRoles.Administrator); if (!string.IsNullOrEmpty(deviceId)) { @@ -106,7 +108,7 @@ public class SessionController : BaseJellyfinApiController return true; }); } - else if (!User.IsInRole(UserRoles.Administrator)) + else if (!isRequestingFromAdmin) { // Request isn't from administrator, limit to "own" sessions. result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(User.GetUserId())); @@ -118,6 +120,16 @@ public class SessionController : BaseJellyfinApiController result = result.Where(i => i.LastActivityDate >= minActiveDate); } + // Request isn't from administrator, don't report acceleration type. + if (!isRequestingFromAdmin) + { + result = result.Select(r => + { + r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none; + return r; + }); + } + return Ok(result); } From 7a2427bf07f9036d62c88a75855cd6dc7e8e3064 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 5 Sep 2024 12:55:15 +0200 Subject: [PATCH 082/159] Add SessionInfoDto, DeviceInfoDto and implement JsonDelimitedArrayConverter.Write --- .../Session/SessionManager.cs | 137 ++++++++++++- Jellyfin.Api/Controllers/DevicesController.cs | 10 +- Jellyfin.Api/Controllers/SessionController.cs | 83 ++------ Jellyfin.Data/Dtos/DeviceOptionsDto.cs | 33 ++-- .../Devices/DeviceManager.cs | 85 ++++++-- .../Authentication/AuthenticationResult.cs | 33 ++-- .../Devices/IDeviceManager.cs | 150 ++++++++------ .../AuthenticationResultEventArgs.cs | 3 +- .../Outbound/SessionsMessage.cs | 5 +- .../Session/ISessionManager.cs | 11 ++ .../Session/SessionInfo.cs | 120 +++++++++-- MediaBrowser.Model/Devices/DeviceInfo.cs | 139 +++++++------ .../Dto}/ClientCapabilitiesDto.cs | 20 +- MediaBrowser.Model/Dto/DeviceInfoDto.cs | 83 ++++++++ MediaBrowser.Model/Dto/SessionInfoDto.cs | 186 ++++++++++++++++++ .../Converters/JsonDelimitedArrayConverter.cs | 65 ++++-- .../JsonCommaDelimitedArrayTests.cs | 16 +- 17 files changed, 862 insertions(+), 317 deletions(-) rename {Jellyfin.Api/Models/SessionDtos => MediaBrowser.Model/Dto}/ClientCapabilitiesDto.cs (77%) create mode 100644 MediaBrowser.Model/Dto/DeviceInfoDto.cs create mode 100644 MediaBrowser.Model/Dto/SessionInfoDto.cs diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 72e164b521..6bcbe3ceba 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -68,13 +66,29 @@ namespace Emby.Server.Implementations.Session private Timer _inactiveTimer; private DtoOptions _itemInfoDtoOptions; - private bool _disposed = false; + private bool _disposed; + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. public SessionManager( ILogger logger, IEventManager eventManager, IUserDataManager userDataManager, - IServerConfigurationManager config, + IServerConfigurationManager serverConfigurationManager, ILibraryManager libraryManager, IUserManager userManager, IMusicManager musicManager, @@ -88,7 +102,7 @@ namespace Emby.Server.Implementations.Session _logger = logger; _eventManager = eventManager; _userDataManager = userDataManager; - _config = config; + _config = serverConfigurationManager; _libraryManager = libraryManager; _userManager = userManager; _musicManager = musicManager; @@ -508,7 +522,10 @@ namespace Emby.Server.Implementations.Session deviceName = "Network Device"; } - var deviceOptions = _deviceManager.GetDeviceOptions(deviceId); + var deviceOptions = _deviceManager.GetDeviceOptions(deviceId) ?? new() + { + DeviceId = deviceId + }; if (string.IsNullOrEmpty(deviceOptions.CustomName)) { sessionInfo.DeviceName = deviceName; @@ -1076,6 +1093,42 @@ namespace Emby.Server.Implementations.Session return session; } + private SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo) + { + return new SessionInfoDto + { + PlayState = sessionInfo.PlayState, + AdditionalUsers = sessionInfo.AdditionalUsers, + Capabilities = _deviceManager.ToClientCapabilitiesDto(sessionInfo.Capabilities), + RemoteEndPoint = sessionInfo.RemoteEndPoint, + PlayableMediaTypes = sessionInfo.PlayableMediaTypes, + Id = sessionInfo.Id, + UserId = sessionInfo.UserId, + UserName = sessionInfo.UserName, + Client = sessionInfo.Client, + LastActivityDate = sessionInfo.LastActivityDate, + LastPlaybackCheckIn = sessionInfo.LastPlaybackCheckIn, + LastPausedDate = sessionInfo.LastPausedDate, + DeviceName = sessionInfo.DeviceName, + DeviceType = sessionInfo.DeviceType, + NowPlayingItem = sessionInfo.NowPlayingItem, + NowViewingItem = sessionInfo.NowViewingItem, + DeviceId = sessionInfo.DeviceId, + ApplicationVersion = sessionInfo.ApplicationVersion, + TranscodingInfo = sessionInfo.TranscodingInfo, + IsActive = sessionInfo.IsActive, + SupportsMediaControl = sessionInfo.SupportsMediaControl, + SupportsRemoteControl = sessionInfo.SupportsRemoteControl, + NowPlayingQueue = sessionInfo.NowPlayingQueue, + NowPlayingQueueFullItems = sessionInfo.NowPlayingQueueFullItems, + HasCustomDeviceName = sessionInfo.HasCustomDeviceName, + PlaylistItemId = sessionInfo.PlaylistItemId, + ServerId = sessionInfo.ServerId, + UserPrimaryImageTag = sessionInfo.UserPrimaryImageTag, + SupportedCommands = sessionInfo.SupportedCommands + }; + } + /// public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, CancellationToken cancellationToken) { @@ -1393,7 +1446,7 @@ namespace Emby.Server.Implementations.Session UserName = user.Username }; - session.AdditionalUsers = [..session.AdditionalUsers, newUser]; + session.AdditionalUsers = [.. session.AdditionalUsers, newUser]; } } @@ -1505,7 +1558,7 @@ namespace Emby.Server.Implementations.Session var returnResult = new AuthenticationResult { User = _userManager.GetUserDto(user, request.RemoteEndPoint), - SessionInfo = session, + SessionInfo = ToSessionInfoDto(session), AccessToken = token, ServerId = _appHost.SystemId }; @@ -1800,6 +1853,74 @@ namespace Emby.Server.Implementations.Session return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false); } + /// + public IReadOnlyList GetSessions( + Guid userId, + string deviceId, + int? activeWithinSeconds, + Guid? controllableUserToCheck) + { + var result = Sessions; + var user = _userManager.GetUserById(userId); + if (!string.IsNullOrEmpty(deviceId)) + { + result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); + } + + if (!controllableUserToCheck.IsNullOrEmpty()) + { + result = result.Where(i => i.SupportsRemoteControl); + + var controlledUser = _userManager.GetUserById(controllableUserToCheck.Value); + if (controlledUser is null) + { + return []; + } + + if (!controlledUser.HasPermission(PermissionKind.EnableSharedDeviceControl)) + { + // Controlled user has device sharing disabled + result = result.Where(i => !i.UserId.IsEmpty()); + } + + if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)) + { + // User cannot control other user's sessions, validate user id. + result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(controllableUserToCheck.Value)); + } + + result = result.Where(i => + { + if (!string.IsNullOrWhiteSpace(i.DeviceId) && !_deviceManager.CanAccessDevice(user, i.DeviceId)) + { + return false; + } + + return true; + }); + } + else if (!user.HasPermission(PermissionKind.IsAdministrator)) + { + // Request isn't from administrator, limit to "own" sessions. + result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId)); + + // Don't report acceleration type for non-admin users. + result = result.Select(r => + { + r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none; + return r; + }); + } + + if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0) + { + var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value); + result = result.Where(i => i.LastActivityDate >= minActiveDate); + } + + return result.Select(ToSessionInfoDto).ToList(); + } + /// public Task SendMessageToAdminSessions(SessionMessageType name, T data, CancellationToken cancellationToken) { diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 2a2ab4ad16..50050262f0 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -1,15 +1,13 @@ using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; -using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Data.Dtos; -using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Queries; using MediaBrowser.Common.Api; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Devices; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -47,7 +45,7 @@ public class DevicesController : BaseJellyfinApiController /// An containing the list of devices. [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetDevices([FromQuery] Guid? userId) + public ActionResult> GetDevices([FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); return _deviceManager.GetDevicesForUser(userId); @@ -63,7 +61,7 @@ public class DevicesController : BaseJellyfinApiController [HttpGet("Info")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDeviceInfo([FromQuery, Required] string id) + public ActionResult GetDeviceInfo([FromQuery, Required] string id) { var deviceInfo = _deviceManager.GetDevice(id); if (deviceInfo is null) @@ -84,7 +82,7 @@ public class DevicesController : BaseJellyfinApiController [HttpGet("Options")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDeviceOptions([FromQuery, Required] string id) + public ActionResult GetDeviceOptions([FromQuery, Required] string id) { var deviceInfo = _deviceManager.GetDeviceOptions(id); if (deviceInfo is null) diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 942bdeb9e8..91a879b8ed 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -1,18 +1,13 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Api.Models.SessionDtos; using Jellyfin.Data.Enums; -using Jellyfin.Extensions; using MediaBrowser.Common.Api; -using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; @@ -32,22 +27,18 @@ public class SessionController : BaseJellyfinApiController { private readonly ISessionManager _sessionManager; private readonly IUserManager _userManager; - private readonly IDeviceManager _deviceManager; /// /// Initializes a new instance of the class. /// /// Instance of interface. /// Instance of interface. - /// Instance of interface. public SessionController( ISessionManager sessionManager, - IUserManager userManager, - IDeviceManager deviceManager) + IUserManager userManager) { _sessionManager = sessionManager; _userManager = userManager; - _deviceManager = deviceManager; } /// @@ -57,77 +48,25 @@ public class SessionController : BaseJellyfinApiController /// Filter by device Id. /// Optional. Filter by sessions that were active in the last n seconds. /// List of sessions returned. - /// An with the available sessions. + /// An with the available sessions. [HttpGet("Sessions")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetSessions( + public ActionResult> GetSessions( [FromQuery] Guid? controllableByUserId, [FromQuery] string? deviceId, [FromQuery] int? activeWithinSeconds) { - var result = _sessionManager.Sessions; - var isRequestingFromAdmin = User.IsInRole(UserRoles.Administrator); + Guid? controllableUserToCheck = controllableByUserId is null ? null : RequestHelpers.GetUserId(User, controllableByUserId); + var result = _sessionManager.GetSessions( + User.GetUserId(), + deviceId, + activeWithinSeconds, + controllableUserToCheck); - if (!string.IsNullOrEmpty(deviceId)) + if (result.Count == 0) { - result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); - } - - if (!controllableByUserId.IsNullOrEmpty()) - { - result = result.Where(i => i.SupportsRemoteControl); - - var user = _userManager.GetUserById(controllableByUserId.Value); - if (user is null) - { - return NotFound(); - } - - if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)) - { - // User cannot control other user's sessions, validate user id. - result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(RequestHelpers.GetUserId(User, controllableByUserId))); - } - - if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl)) - { - result = result.Where(i => !i.UserId.IsEmpty()); - } - - result = result.Where(i => - { - if (!string.IsNullOrWhiteSpace(i.DeviceId)) - { - if (!_deviceManager.CanAccessDevice(user, i.DeviceId)) - { - return false; - } - } - - return true; - }); - } - else if (!isRequestingFromAdmin) - { - // Request isn't from administrator, limit to "own" sessions. - result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(User.GetUserId())); - } - - if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0) - { - var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value); - result = result.Where(i => i.LastActivityDate >= minActiveDate); - } - - // Request isn't from administrator, don't report acceleration type. - if (!isRequestingFromAdmin) - { - result = result.Select(r => - { - r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none; - return r; - }); + return NotFound(); } return Ok(result); diff --git a/Jellyfin.Data/Dtos/DeviceOptionsDto.cs b/Jellyfin.Data/Dtos/DeviceOptionsDto.cs index 392ef5ff4e..aad5787097 100644 --- a/Jellyfin.Data/Dtos/DeviceOptionsDto.cs +++ b/Jellyfin.Data/Dtos/DeviceOptionsDto.cs @@ -1,23 +1,22 @@ -namespace Jellyfin.Data.Dtos +namespace Jellyfin.Data.Dtos; + +/// +/// A dto representing custom options for a device. +/// +public class DeviceOptionsDto { /// - /// A dto representing custom options for a device. + /// Gets or sets the id. /// - public class DeviceOptionsDto - { - /// - /// Gets or sets the id. - /// - public int Id { get; set; } + public int Id { get; set; } - /// - /// Gets or sets the device id. - /// - public string? DeviceId { get; set; } + /// + /// Gets or sets the device id. + /// + public string? DeviceId { get; set; } - /// - /// Gets or sets the custom name. - /// - public string? CustomName { get; set; } - } + /// + /// Gets or sets the custom name. + /// + public string? CustomName { get; set; } } diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 415c04bbf1..d3bff2936c 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Data.Dtos; using Jellyfin.Data.Entities; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Enums; @@ -13,6 +14,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Devices; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; using Microsoft.EntityFrameworkCore; @@ -68,7 +70,7 @@ namespace Jellyfin.Server.Implementations.Devices } /// - public async Task UpdateDeviceOptions(string deviceId, string deviceName) + public async Task UpdateDeviceOptions(string deviceId, string? deviceName) { DeviceOptions? deviceOptions; var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); @@ -105,29 +107,37 @@ namespace Jellyfin.Server.Implementations.Devices } /// - public DeviceOptions GetDeviceOptions(string deviceId) + public DeviceOptionsDto? GetDeviceOptions(string deviceId) { - _deviceOptions.TryGetValue(deviceId, out var deviceOptions); + if (_deviceOptions.TryGetValue(deviceId, out var deviceOptions)) + { + return ToDeviceOptionsDto(deviceOptions); + } - return deviceOptions ?? new DeviceOptions(deviceId); + return null; } /// - public ClientCapabilities GetCapabilities(string deviceId) + public ClientCapabilities GetCapabilities(string? deviceId) { + if (deviceId is null) + { + return new(); + } + return _capabilitiesMap.TryGetValue(deviceId, out ClientCapabilities? result) ? result - : new ClientCapabilities(); + : new(); } /// - public DeviceInfo? GetDevice(string id) + public DeviceInfoDto? GetDevice(string id) { var device = _devices.Values.Where(d => d.DeviceId == id).OrderByDescending(d => d.DateLastActivity).FirstOrDefault(); _deviceOptions.TryGetValue(id, out var deviceOption); var deviceInfo = device is null ? null : ToDeviceInfo(device, deviceOption); - return deviceInfo; + return deviceInfo is null ? null : ToDeviceInfoDto(deviceInfo); } /// @@ -166,7 +176,7 @@ namespace Jellyfin.Server.Implementations.Devices } /// - public QueryResult GetDevicesForUser(Guid? userId) + public QueryResult GetDevicesForUser(Guid? userId) { IEnumerable devices = _devices.Values .OrderByDescending(d => d.DateLastActivity) @@ -187,9 +197,11 @@ namespace Jellyfin.Server.Implementations.Devices { _deviceOptions.TryGetValue(device.DeviceId, out var option); return ToDeviceInfo(device, option); - }).ToArray(); + }) + .Select(ToDeviceInfoDto) + .ToArray(); - return new QueryResult(array); + return new QueryResult(array); } /// @@ -235,13 +247,9 @@ namespace Jellyfin.Server.Implementations.Devices private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null) { var caps = GetCapabilities(authInfo.DeviceId); - var user = _userManager.GetUserById(authInfo.UserId); - if (user is null) - { - throw new ResourceNotFoundException("User with UserId " + authInfo.UserId + " not found"); - } + var user = _userManager.GetUserById(authInfo.UserId) ?? throw new ResourceNotFoundException("User with UserId " + authInfo.UserId + " not found"); - return new DeviceInfo + return new() { AppName = authInfo.AppName, AppVersion = authInfo.AppVersion, @@ -254,5 +262,48 @@ namespace Jellyfin.Server.Implementations.Devices CustomName = options?.CustomName, }; } + + private DeviceOptionsDto ToDeviceOptionsDto(DeviceOptions options) + { + return new() + { + Id = options.Id, + DeviceId = options.DeviceId, + CustomName = options.CustomName, + }; + } + + private DeviceInfoDto ToDeviceInfoDto(DeviceInfo info) + { + return new() + { + Name = info.Name, + CustomName = info.CustomName, + AccessToken = info.AccessToken, + Id = info.Id, + LastUserName = info.LastUserName, + AppName = info.AppName, + AppVersion = info.AppVersion, + LastUserId = info.LastUserId, + DateLastActivity = info.DateLastActivity, + Capabilities = ToClientCapabilitiesDto(info.Capabilities), + IconUrl = info.IconUrl + }; + } + + /// + public ClientCapabilitiesDto ToClientCapabilitiesDto(ClientCapabilities capabilities) + { + return new() + { + PlayableMediaTypes = capabilities.PlayableMediaTypes, + SupportedCommands = capabilities.SupportedCommands, + SupportsMediaControl = capabilities.SupportsMediaControl, + SupportsPersistentIdentifier = capabilities.SupportsPersistentIdentifier, + DeviceProfile = capabilities.DeviceProfile, + AppStoreUrl = capabilities.AppStoreUrl, + IconUrl = capabilities.IconUrl + }; + } } } diff --git a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs index 635e4eb3d7..daf4d96313 100644 --- a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs +++ b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs @@ -1,20 +1,31 @@ #nullable disable -#pragma warning disable CS1591 - -using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; -namespace MediaBrowser.Controller.Authentication +namespace MediaBrowser.Controller.Authentication; + +/// +/// A class representing an authentication result. +/// +public class AuthenticationResult { - public class AuthenticationResult - { - public UserDto User { get; set; } + /// + /// Gets or sets the user. + /// + public UserDto User { get; set; } - public SessionInfo SessionInfo { get; set; } + /// + /// Gets or sets the session info. + /// + public SessionInfoDto SessionInfo { get; set; } - public string AccessToken { get; set; } + /// + /// Gets or sets the access token. + /// + public string AccessToken { get; set; } - public string ServerId { get; set; } - } + /// + /// Gets or sets the server id. + /// + public string ServerId { get; set; } } diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 5566421cbe..cade53d994 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -1,81 +1,117 @@ -#nullable disable - -#pragma warning disable CS1591 - using System; using System.Threading.Tasks; +using Jellyfin.Data.Dtos; using Jellyfin.Data.Entities; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; using MediaBrowser.Model.Devices; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; -namespace MediaBrowser.Controller.Devices +namespace MediaBrowser.Controller.Devices; + +/// +/// Device manager interface. +/// +public interface IDeviceManager { - public interface IDeviceManager - { - event EventHandler>> DeviceOptionsUpdated; + /// + /// Event handler for updated device options. + /// + event EventHandler>> DeviceOptionsUpdated; - /// - /// Creates a new device. - /// - /// The device to create. - /// A representing the creation of the device. - Task CreateDevice(Device device); + /// + /// Creates a new device. + /// + /// The device to create. + /// A representing the creation of the device. + Task CreateDevice(Device device); - /// - /// Saves the capabilities. - /// - /// The device id. - /// The capabilities. - void SaveCapabilities(string deviceId, ClientCapabilities capabilities); + /// + /// Saves the capabilities. + /// + /// The device id. + /// The capabilities. + void SaveCapabilities(string deviceId, ClientCapabilities capabilities); - /// - /// Gets the capabilities. - /// - /// The device id. - /// ClientCapabilities. - ClientCapabilities GetCapabilities(string deviceId); + /// + /// Gets the capabilities. + /// + /// The device id. + /// ClientCapabilities. + ClientCapabilities GetCapabilities(string? deviceId); - /// - /// Gets the device information. - /// - /// The identifier. - /// DeviceInfo. - DeviceInfo GetDevice(string id); + /// + /// Gets the device information. + /// + /// The identifier. + /// DeviceInfoDto. + DeviceInfoDto? GetDevice(string id); - /// - /// Gets devices based on the provided query. - /// - /// The device query. - /// A representing the retrieval of the devices. - QueryResult GetDevices(DeviceQuery query); + /// + /// Gets devices based on the provided query. + /// + /// The device query. + /// A representing the retrieval of the devices. + QueryResult GetDevices(DeviceQuery query); - QueryResult GetDeviceInfos(DeviceQuery query); + /// + /// Gets device infromation based on the provided query. + /// + /// The device query. + /// A representing the retrieval of the device information. + QueryResult GetDeviceInfos(DeviceQuery query); - /// - /// Gets the devices. - /// - /// The user's id, or null. - /// IEnumerable<DeviceInfo>. - QueryResult GetDevicesForUser(Guid? userId); + /// + /// Gets the device information. + /// + /// The user's id, or null. + /// IEnumerable<DeviceInfoDto>. + QueryResult GetDevicesForUser(Guid? userId); - Task DeleteDevice(Device device); + /// + /// Deletes a device. + /// + /// The device. + /// A representing the deletion of the device. + Task DeleteDevice(Device device); - Task UpdateDevice(Device device); + /// + /// Updates a device. + /// + /// The device. + /// A representing the update of the device. + Task UpdateDevice(Device device); - /// - /// Determines whether this instance [can access device] the specified user identifier. - /// - /// The user to test. - /// The device id to test. - /// Whether the user can access the device. - bool CanAccessDevice(User user, string deviceId); + /// + /// Determines whether this instance [can access device] the specified user identifier. + /// + /// The user to test. + /// The device id to test. + /// Whether the user can access the device. + bool CanAccessDevice(User user, string deviceId); - Task UpdateDeviceOptions(string deviceId, string deviceName); + /// + /// Updates the options of a device. + /// + /// The device id. + /// The device name. + /// A representing the update of the device options. + Task UpdateDeviceOptions(string deviceId, string? deviceName); - DeviceOptions GetDeviceOptions(string deviceId); - } + /// + /// Gets the options of a device. + /// + /// The device id. + /// of the device. + DeviceOptionsDto? GetDeviceOptions(string deviceId); + + /// + /// Gets the dto for client capabilites. + /// + /// The client capabilities. + /// of the device. + ClientCapabilitiesDto ToClientCapabilitiesDto(ClientCapabilities capabilities); } diff --git a/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs b/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs index 357ef9406d..1542c58b35 100644 --- a/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs +++ b/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs @@ -1,6 +1,5 @@ using System; using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; namespace MediaBrowser.Controller.Events.Authentication; @@ -29,7 +28,7 @@ public class AuthenticationResultEventArgs : EventArgs /// /// Gets or sets the session information. /// - public SessionInfo? SessionInfo { get; set; } + public SessionInfoDto? SessionInfo { get; set; } /// /// Gets or sets the server id. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs index 3504831b87..8330745418 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.ComponentModel; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Session; namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; @@ -8,13 +9,13 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// /// Sessions message. /// -public class SessionsMessage : OutboundWebSocketMessage> +public class SessionsMessage : OutboundWebSocketMessage> { /// /// Initializes a new instance of the class. /// /// Session info. - public SessionsMessage(IReadOnlyList data) + public SessionsMessage(IReadOnlyList data) : base(data) { } diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 5a47236f92..f2e98dd787 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities.Security; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Session; using MediaBrowser.Model.SyncPlay; @@ -292,6 +293,16 @@ namespace MediaBrowser.Controller.Session /// SessionInfo. SessionInfo GetSession(string deviceId, string client, string version); + /// + /// Gets all sessions available to a user. + /// + /// The session identifier. + /// The device id. + /// Active within session limit. + /// Filter for sessions remote controllable for this user. + /// IReadOnlyList{SessionInfoDto}. + IReadOnlyList GetSessions(Guid userId, string deviceId, int? activeWithinSeconds, Guid? controllableUserToCheck); + /// /// Gets the session by authentication token. /// diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 9e33588187..3ba1bfce42 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Linq; @@ -27,28 +25,45 @@ namespace MediaBrowser.Controller.Session private readonly ISessionManager _sessionManager; private readonly ILogger _logger; - private readonly object _progressLock = new object(); + private readonly object _progressLock = new(); private Timer _progressTimer; private PlaybackProgressInfo _lastProgressInfo; - private bool _disposed = false; + private bool _disposed; + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. public SessionInfo(ISessionManager sessionManager, ILogger logger) { _sessionManager = sessionManager; _logger = logger; - AdditionalUsers = Array.Empty(); + AdditionalUsers = []; PlayState = new PlayerStateInfo(); - SessionControllers = Array.Empty(); - NowPlayingQueue = Array.Empty(); - NowPlayingQueueFullItems = Array.Empty(); + SessionControllers = []; + NowPlayingQueue = []; + NowPlayingQueueFullItems = []; } + /// + /// Gets or sets the play state. + /// + /// The play state. public PlayerStateInfo PlayState { get; set; } - public SessionUserInfo[] AdditionalUsers { get; set; } + /// + /// Gets or sets the additional users. + /// + /// The additional users. + public IReadOnlyList AdditionalUsers { get; set; } + /// + /// Gets or sets the client capabilities. + /// + /// The client capabilities. public ClientCapabilities Capabilities { get; set; } /// @@ -67,7 +82,7 @@ namespace MediaBrowser.Controller.Session { if (Capabilities is null) { - return Array.Empty(); + return []; } return Capabilities.PlayableMediaTypes; @@ -134,9 +149,17 @@ namespace MediaBrowser.Controller.Session /// The now playing item. public BaseItemDto NowPlayingItem { get; set; } + /// + /// Gets or sets the now playing queue full items. + /// + /// The now playing queue full items. [JsonIgnore] public BaseItem FullNowPlayingItem { get; set; } + /// + /// Gets or sets the now viewing item. + /// + /// The now viewing item. public BaseItemDto NowViewingItem { get; set; } /// @@ -156,8 +179,12 @@ namespace MediaBrowser.Controller.Session /// /// The session controller. [JsonIgnore] - public ISessionController[] SessionControllers { get; set; } + public IReadOnlyList SessionControllers { get; set; } + /// + /// Gets or sets the transcoding info. + /// + /// The transcoding info. public TranscodingInfo TranscodingInfo { get; set; } /// @@ -177,7 +204,7 @@ namespace MediaBrowser.Controller.Session } } - if (controllers.Length > 0) + if (controllers.Count > 0) { return false; } @@ -186,6 +213,10 @@ namespace MediaBrowser.Controller.Session } } + /// + /// Gets a value indicating whether the session supports media control. + /// + /// true if this session supports media control; otherwise, false. public bool SupportsMediaControl { get @@ -208,6 +239,10 @@ namespace MediaBrowser.Controller.Session } } + /// + /// Gets a value indicating whether the session supports remote control. + /// + /// true if this session supports remote control; otherwise, false. public bool SupportsRemoteControl { get @@ -230,16 +265,40 @@ namespace MediaBrowser.Controller.Session } } + /// + /// Gets or sets the now playing queue. + /// + /// The now playing queue. public IReadOnlyList NowPlayingQueue { get; set; } + /// + /// Gets or sets the now playing queue full items. + /// + /// The now playing queue full items. public IReadOnlyList NowPlayingQueueFullItems { get; set; } + /// + /// Gets or sets a value indicating whether the session has a custom device name. + /// + /// true if this session has a custom device name; otherwise, false. public bool HasCustomDeviceName { get; set; } + /// + /// Gets or sets the playlist item id. + /// + /// The splaylist item id. public string PlaylistItemId { get; set; } + /// + /// Gets or sets the server id. + /// + /// The server id. public string ServerId { get; set; } + /// + /// Gets or sets the user primary image tag. + /// + /// The user primary image tag. public string UserPrimaryImageTag { get; set; } /// @@ -247,8 +306,14 @@ namespace MediaBrowser.Controller.Session /// /// The supported commands. public IReadOnlyList SupportedCommands - => Capabilities is null ? Array.Empty() : Capabilities.SupportedCommands; + => Capabilities is null ? [] : Capabilities.SupportedCommands; + /// + /// Ensures a controller of type exists. + /// + /// Class to register. + /// The factory. + /// Tuple{ISessionController, bool}. public Tuple EnsureController(Func factory) { var controllers = SessionControllers.ToList(); @@ -261,18 +326,27 @@ namespace MediaBrowser.Controller.Session } var newController = factory(this); - _logger.LogDebug("Creating new {0}", newController.GetType().Name); + _logger.LogDebug("Creating new {Factory}", newController.GetType().Name); controllers.Add(newController); - SessionControllers = controllers.ToArray(); + SessionControllers = [.. controllers]; return new Tuple(newController, true); } + /// + /// Adds a controller to the session. + /// + /// The controller. public void AddController(ISessionController controller) { - SessionControllers = [..SessionControllers, controller]; + SessionControllers = [.. SessionControllers, controller]; } + /// + /// Gets a value indicating whether the session contains a user. + /// + /// The user id to check. + /// true if this session contains the user; otherwise, false. public bool ContainsUser(Guid userId) { if (UserId.Equals(userId)) @@ -291,6 +365,11 @@ namespace MediaBrowser.Controller.Session return false; } + /// + /// Starts automatic progressing. + /// + /// The playback progress info. + /// The supported commands. public void StartAutomaticProgress(PlaybackProgressInfo progressInfo) { if (_disposed) @@ -359,6 +438,9 @@ namespace MediaBrowser.Controller.Session } } + /// + /// Stops automatic progressing. + /// public void StopAutomaticProgress() { lock (_progressLock) @@ -373,6 +455,10 @@ namespace MediaBrowser.Controller.Session } } + /// + /// Disposes the instance async. + /// + /// ValueTask. public async ValueTask DisposeAsync() { _disposed = true; @@ -380,7 +466,7 @@ namespace MediaBrowser.Controller.Session StopAutomaticProgress(); var controllers = SessionControllers.ToList(); - SessionControllers = Array.Empty(); + SessionControllers = []; foreach (var controller in controllers) { diff --git a/MediaBrowser.Model/Devices/DeviceInfo.cs b/MediaBrowser.Model/Devices/DeviceInfo.cs index 4962992a0a..1155986138 100644 --- a/MediaBrowser.Model/Devices/DeviceInfo.cs +++ b/MediaBrowser.Model/Devices/DeviceInfo.cs @@ -1,69 +1,84 @@ -#nullable disable -#pragma warning disable CS1591 - using System; using MediaBrowser.Model.Session; -namespace MediaBrowser.Model.Devices +namespace MediaBrowser.Model.Devices; + +/// +/// A class for device Information. +/// +public class DeviceInfo { - public class DeviceInfo + /// + /// Initializes a new instance of the class. + /// + public DeviceInfo() { - public DeviceInfo() - { - Capabilities = new ClientCapabilities(); - } - - public string Name { get; set; } - - public string CustomName { get; set; } - - /// - /// Gets or sets the access token. - /// - public string AccessToken { get; set; } - - /// - /// Gets or sets the identifier. - /// - /// The identifier. - public string Id { get; set; } - - /// - /// Gets or sets the last name of the user. - /// - /// The last name of the user. - public string LastUserName { get; set; } - - /// - /// Gets or sets the name of the application. - /// - /// The name of the application. - public string AppName { get; set; } - - /// - /// Gets or sets the application version. - /// - /// The application version. - public string AppVersion { get; set; } - - /// - /// Gets or sets the last user identifier. - /// - /// The last user identifier. - public Guid LastUserId { get; set; } - - /// - /// Gets or sets the date last modified. - /// - /// The date last modified. - public DateTime DateLastActivity { get; set; } - - /// - /// Gets or sets the capabilities. - /// - /// The capabilities. - public ClientCapabilities Capabilities { get; set; } - - public string IconUrl { get; set; } + Capabilities = new ClientCapabilities(); } + + /// + /// Gets or sets the name. + /// + /// The name. + public string? Name { get; set; } + + /// + /// Gets or sets the custom name. + /// + /// The custom name. + public string? CustomName { get; set; } + + /// + /// Gets or sets the access token. + /// + /// The access token. + public string? AccessToken { get; set; } + + /// + /// Gets or sets the identifier. + /// + /// The identifier. + public string? Id { get; set; } + + /// + /// Gets or sets the last name of the user. + /// + /// The last name of the user. + public string? LastUserName { get; set; } + + /// + /// Gets or sets the name of the application. + /// + /// The name of the application. + public string? AppName { get; set; } + + /// + /// Gets or sets the application version. + /// + /// The application version. + public string? AppVersion { get; set; } + + /// + /// Gets or sets the last user identifier. + /// + /// The last user identifier. + public Guid? LastUserId { get; set; } + + /// + /// Gets or sets the date last modified. + /// + /// The date last modified. + public DateTime? DateLastActivity { get; set; } + + /// + /// Gets or sets the capabilities. + /// + /// The capabilities. + public ClientCapabilities Capabilities { get; set; } + + /// + /// Gets or sets the icon URL. + /// + /// The icon URL. + public string? IconUrl { get; set; } } diff --git a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs b/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs similarity index 77% rename from Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs rename to MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs index c699c469d9..5963ed270d 100644 --- a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs +++ b/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs @@ -1,13 +1,11 @@ -using System; using System.Collections.Generic; -using System.ComponentModel; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Session; -namespace Jellyfin.Api.Models.SessionDtos; +namespace MediaBrowser.Model.Dto; /// /// Client capabilities dto. @@ -18,13 +16,13 @@ public class ClientCapabilitiesDto /// Gets or sets the list of playable media types. /// [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList PlayableMediaTypes { get; set; } = Array.Empty(); + public IReadOnlyList PlayableMediaTypes { get; set; } = []; /// /// Gets or sets the list of supported commands. /// [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList SupportedCommands { get; set; } = Array.Empty(); + public IReadOnlyList SupportedCommands { get; set; } = []; /// /// Gets or sets a value indicating whether session supports media control. @@ -51,18 +49,6 @@ public class ClientCapabilitiesDto /// public string? IconUrl { get; set; } -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - // TODO: Remove after 10.9 - [Obsolete("Unused")] - [DefaultValue(false)] - public bool? SupportsContentUploading { get; set; } = false; - - // TODO: Remove after 10.9 - [Obsolete("Unused")] - [DefaultValue(false)] - public bool? SupportsSync { get; set; } = false; -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member - /// /// Convert the dto to the full model. /// diff --git a/MediaBrowser.Model/Dto/DeviceInfoDto.cs b/MediaBrowser.Model/Dto/DeviceInfoDto.cs new file mode 100644 index 0000000000..ac7a731a90 --- /dev/null +++ b/MediaBrowser.Model/Dto/DeviceInfoDto.cs @@ -0,0 +1,83 @@ +using System; + +namespace MediaBrowser.Model.Dto; + +/// +/// A DTO representing device information. +/// +public class DeviceInfoDto +{ + /// + /// Initializes a new instance of the class. + /// + public DeviceInfoDto() + { + Capabilities = new ClientCapabilitiesDto(); + } + + /// + /// Gets or sets the name. + /// + /// The name. + public string? Name { get; set; } + + /// + /// Gets or sets the custom name. + /// + /// The custom name. + public string? CustomName { get; set; } + + /// + /// Gets or sets the access token. + /// + /// The access token. + public string? AccessToken { get; set; } + + /// + /// Gets or sets the identifier. + /// + /// The identifier. + public string? Id { get; set; } + + /// + /// Gets or sets the last name of the user. + /// + /// The last name of the user. + public string? LastUserName { get; set; } + + /// + /// Gets or sets the name of the application. + /// + /// The name of the application. + public string? AppName { get; set; } + + /// + /// Gets or sets the application version. + /// + /// The application version. + public string? AppVersion { get; set; } + + /// + /// Gets or sets the last user identifier. + /// + /// The last user identifier. + public Guid? LastUserId { get; set; } + + /// + /// Gets or sets the date last modified. + /// + /// The date last modified. + public DateTime? DateLastActivity { get; set; } + + /// + /// Gets or sets the capabilities. + /// + /// The capabilities. + public ClientCapabilitiesDto Capabilities { get; set; } + + /// + /// Gets or sets the icon URL. + /// + /// The icon URL. + public string? IconUrl { get; set; } +} diff --git a/MediaBrowser.Model/Dto/SessionInfoDto.cs b/MediaBrowser.Model/Dto/SessionInfoDto.cs new file mode 100644 index 0000000000..2496c933a2 --- /dev/null +++ b/MediaBrowser.Model/Dto/SessionInfoDto.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Data.Enums; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Model.Dto; + +/// +/// Session info DTO. +/// +public class SessionInfoDto +{ + /// + /// Gets or sets the play state. + /// + /// The play state. + public PlayerStateInfo? PlayState { get; set; } + + /// + /// Gets or sets the additional users. + /// + /// The additional users. + public IReadOnlyList? AdditionalUsers { get; set; } + + /// + /// Gets or sets the client capabilities. + /// + /// The client capabilities. + public ClientCapabilitiesDto? Capabilities { get; set; } + + /// + /// Gets or sets the remote end point. + /// + /// The remote end point. + public string? RemoteEndPoint { get; set; } + + /// + /// Gets or sets the playable media types. + /// + /// The playable media types. + public IReadOnlyList PlayableMediaTypes { get; set; } = []; + + /// + /// Gets or sets the id. + /// + /// The id. + public string? Id { get; set; } + + /// + /// Gets or sets the user id. + /// + /// The user id. + public Guid UserId { get; set; } + + /// + /// Gets or sets the username. + /// + /// The username. + public string? UserName { get; set; } + + /// + /// Gets or sets the type of the client. + /// + /// The type of the client. + public string? Client { get; set; } + + /// + /// Gets or sets the last activity date. + /// + /// The last activity date. + public DateTime LastActivityDate { get; set; } + + /// + /// Gets or sets the last playback check in. + /// + /// The last playback check in. + public DateTime LastPlaybackCheckIn { get; set; } + + /// + /// Gets or sets the last paused date. + /// + /// The last paused date. + public DateTime? LastPausedDate { get; set; } + + /// + /// Gets or sets the name of the device. + /// + /// The name of the device. + public string? DeviceName { get; set; } + + /// + /// Gets or sets the type of the device. + /// + /// The type of the device. + public string? DeviceType { get; set; } + + /// + /// Gets or sets the now playing item. + /// + /// The now playing item. + public BaseItemDto? NowPlayingItem { get; set; } + + /// + /// Gets or sets the now viewing item. + /// + /// The now viewing item. + public BaseItemDto? NowViewingItem { get; set; } + + /// + /// Gets or sets the device id. + /// + /// The device id. + public string? DeviceId { get; set; } + + /// + /// Gets or sets the application version. + /// + /// The application version. + public string? ApplicationVersion { get; set; } + + /// + /// Gets or sets the transcoding info. + /// + /// The transcoding info. + public TranscodingInfo? TranscodingInfo { get; set; } + + /// + /// Gets or sets a value indicating whether this session is active. + /// + /// true if this session is active; otherwise, false. + public bool IsActive { get; set; } + + /// + /// Gets or sets a value indicating whether the session supports media control. + /// + /// true if this session supports media control; otherwise, false. + public bool SupportsMediaControl { get; set; } + + /// + /// Gets or sets a value indicating whether the session supports remote control. + /// + /// true if this session supports remote control; otherwise, false. + public bool SupportsRemoteControl { get; set; } + + /// + /// Gets or sets the now playing queue. + /// + /// The now playing queue. + public IReadOnlyList? NowPlayingQueue { get; set; } + + /// + /// Gets or sets the now playing queue full items. + /// + /// The now playing queue full items. + public IReadOnlyList? NowPlayingQueueFullItems { get; set; } + + /// + /// Gets or sets a value indicating whether the session has a custom device name. + /// + /// true if this session has a custom device name; otherwise, false. + public bool HasCustomDeviceName { get; set; } + + /// + /// Gets or sets the playlist item id. + /// + /// The splaylist item id. + public string? PlaylistItemId { get; set; } + + /// + /// Gets or sets the server id. + /// + /// The server id. + public string? ServerId { get; set; } + + /// + /// Gets or sets the user primary image tag. + /// + /// The user primary image tag. + public string? UserPrimaryImageTag { get; set; } + + /// + /// Gets or sets the supported commands. + /// + /// The supported commands. + public IReadOnlyList SupportedCommands { get; set; } = []; +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs index 1466d3a71a..b9477ce6b7 100644 --- a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs +++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; @@ -35,38 +37,27 @@ namespace Jellyfin.Extensions.Json.Converters var stringEntries = reader.GetString()!.Split(Delimiter, StringSplitOptions.RemoveEmptyEntries); if (stringEntries.Length == 0) { - return Array.Empty(); + return []; } - var parsedValues = new object[stringEntries.Length]; - var convertedCount = 0; + var typedValues = new List(); for (var i = 0; i < stringEntries.Length; i++) { try { - parsedValues[i] = _typeConverter.ConvertFromInvariantString(stringEntries[i].Trim()) ?? throw new FormatException(); - convertedCount++; + var parsedValue = _typeConverter.ConvertFromInvariantString(stringEntries[i].Trim()); + if (parsedValue is not null) + { + typedValues.Add((T)parsedValue); + } } catch (FormatException) { - // TODO log when upgraded to .Net6 - // https://github.com/dotnet/runtime/issues/42975 - // _logger.LogDebug(e, "Error converting value."); + // Ignore unconvertable inputs } } - var typedValues = new T[convertedCount]; - var typedValueIndex = 0; - for (var i = 0; i < stringEntries.Length; i++) - { - if (parsedValues[i] is not null) - { - typedValues.SetValue(parsedValues[i], typedValueIndex); - typedValueIndex++; - } - } - - return typedValues; + return [.. typedValues]; } return JsonSerializer.Deserialize(ref reader, options); @@ -75,7 +66,39 @@ namespace Jellyfin.Extensions.Json.Converters /// public override void Write(Utf8JsonWriter writer, T[]? value, JsonSerializerOptions options) { - throw new NotImplementedException(); + if (value is not null) + { + writer.WriteStartArray(); + if (value.Length > 0) + { + var toWrite = value.Length - 1; + foreach (var it in value) + { + var wrote = false; + if (it is not null) + { + writer.WriteStringValue(it.ToString()); + wrote = true; + } + + if (toWrite > 0) + { + if (wrote) + { + writer.WriteStringValue(Delimiter.ToString()); + } + + toWrite--; + } + } + } + + writer.WriteEndArray(); + } + else + { + writer.WriteNullValue(); + } } } } diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs index 61105b42b2..9fc0158235 100644 --- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs @@ -41,7 +41,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { "a", "b", "c" } + Value = ["a", "b", "c"] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a,b,c"" }", _jsonOptions); @@ -53,7 +53,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { "a", "b", "c" } + Value = ["a", "b", "c"] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a, b, c"" }", _jsonOptions); @@ -65,7 +65,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,MoveDown"" }", _jsonOptions); @@ -77,7 +77,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,,MoveDown"" }", _jsonOptions); @@ -89,7 +89,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,TotallyNotAVallidCommand,MoveDown"" }", _jsonOptions); @@ -101,7 +101,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp, MoveDown"" }", _jsonOptions); @@ -113,7 +113,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { "a", "b", "c" } + Value = ["a", "b", "c"] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""a"",""b"",""c""] }", _jsonOptions); @@ -125,7 +125,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions); From 0a982e2bfdd6f72dbe9c0bcb09db9890a314a7af Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 18 Sep 2024 16:04:29 +0200 Subject: [PATCH 083/159] Return empty response instead of not found --- Jellyfin.Api/Controllers/SessionController.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 91a879b8ed..72eb93eff2 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -64,11 +64,6 @@ public class SessionController : BaseJellyfinApiController activeWithinSeconds, controllableUserToCheck); - if (result.Count == 0) - { - return NotFound(); - } - return Ok(result); } From ffa1c370fd4b92df15609cd3706b8ebcff930e0d Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 18 Sep 2024 16:10:13 +0200 Subject: [PATCH 084/159] Fix permission checks --- Emby.Server.Implementations/Session/SessionManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 6bcbe3ceba..55e4856692 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1886,7 +1886,7 @@ namespace Emby.Server.Implementations.Session if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)) { // User cannot control other user's sessions, validate user id. - result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(controllableUserToCheck.Value)); + result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(user.Id)); } result = result.Where(i => @@ -1903,7 +1903,10 @@ namespace Emby.Server.Implementations.Session { // Request isn't from administrator, limit to "own" sessions. result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId)); + } + if (!user.HasPermission(PermissionKind.IsAdministrator)) + { // Don't report acceleration type for non-admin users. result = result.Select(r => { From 5bfb7b5d1143f5bbf60d91f645f2bc78d4626016 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 18 Sep 2024 16:18:14 +0200 Subject: [PATCH 085/159] Remove invalid test --- .../Controllers/SessionControllerTests.cs | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs deleted file mode 100644 index c267d3dd35..0000000000 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Net; -using System.Threading.Tasks; -using Xunit; - -namespace Jellyfin.Server.Integration.Tests.Controllers; - -public class SessionControllerTests : IClassFixture -{ - private readonly JellyfinApplicationFactory _factory; - private static string? _accessToken; - - public SessionControllerTests(JellyfinApplicationFactory factory) - { - _factory = factory; - } - - [Fact] - public async Task GetSessions_NonExistentUserId_NotFound() - { - var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - - using var response = await client.GetAsync($"Sessions?controllableByUserId={Guid.NewGuid()}"); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } -} From 9c4bf48b4ea85aac30d96d70a198247e56a6323d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Mesk=C3=B3?= Date: Wed, 18 Sep 2024 15:29:42 +0000 Subject: [PATCH 086/159] Translated using Weblate (Hungarian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hu/ --- .../Localization/Core/hu.json | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index 2c8533ac65..f205e8b64c 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -1,13 +1,13 @@ { "Albums": "Albumok", - "AppDeviceValues": "Program: {0}, Eszköz: {1}", + "AppDeviceValues": "Program: {0}, eszköz: {1}", "Application": "Alkalmazás", "Artists": "Előadók", "AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve", "Books": "Könyvek", "CameraImageUploadedFrom": "Új kamerakép lett feltöltve innen: {0}", "Channels": "Csatornák", - "ChapterNameValue": "Jelenet {0}", + "ChapterNameValue": "{0}. jelenet", "Collections": "Gyűjtemények", "DeviceOfflineWithName": "{0} kijelentkezett", "DeviceOnlineWithName": "{0} belépett", @@ -15,31 +15,31 @@ "Favorites": "Kedvencek", "Folders": "Könyvtárak", "Genres": "Műfajok", - "HeaderAlbumArtists": "Album előadók", + "HeaderAlbumArtists": "Albumelőadók", "HeaderContinueWatching": "Megtekintés folytatása", - "HeaderFavoriteAlbums": "Kedvenc Albumok", - "HeaderFavoriteArtists": "Kedvenc Előadók", - "HeaderFavoriteEpisodes": "Kedvenc Epizódok", - "HeaderFavoriteShows": "Kedvenc Sorozatok", - "HeaderFavoriteSongs": "Kedvenc Dalok", + "HeaderFavoriteAlbums": "Kedvenc albumok", + "HeaderFavoriteArtists": "Kedvenc előadók", + "HeaderFavoriteEpisodes": "Kedvenc epizódok", + "HeaderFavoriteShows": "Kedvenc sorozatok", + "HeaderFavoriteSongs": "Kedvenc számok", "HeaderLiveTV": "Élő TV", "HeaderNextUp": "Következik", - "HeaderRecordingGroups": "Felvevő Csoportok", - "HomeVideos": "Otthoni Videók", - "Inherit": "Örökölt", - "ItemAddedWithName": "{0} hozzáadva a könyvtárhoz", - "ItemRemovedWithName": "{0} eltávolítva a könyvtárból", + "HeaderRecordingGroups": "Felvételi csoportok", + "HomeVideos": "Otthoni videók", + "Inherit": "Öröklés", + "ItemAddedWithName": "{0} hozzáadva a médiatárhoz", + "ItemRemovedWithName": "{0} eltávolítva a médiatárból", "LabelIpAddressValue": "IP-cím: {0}", "LabelRunningTimeValue": "Lejátszási idő: {0}", "Latest": "Legújabb", "MessageApplicationUpdated": "A Jellyfin kiszolgáló frissítve lett", "MessageApplicationUpdatedTo": "A Jellyfin kiszolgáló frissítve lett a következőre: {0}", "MessageNamedServerConfigurationUpdatedWithValue": "A kiszolgálókonfigurációs rész frissítve lett: {0}", - "MessageServerConfigurationUpdated": "Kiszolgálókonfiguráció frissítve lett", + "MessageServerConfigurationUpdated": "A kiszolgálókonfiguráció frissítve lett", "MixedContent": "Vegyes tartalom", "Movies": "Filmek", "Music": "Zenék", - "MusicVideos": "Zenei videóklippek", + "MusicVideos": "Zenei videóklipek", "NameInstallFailed": "{0} sikertelen telepítés", "NameSeasonNumber": "{0}. évad", "NameSeasonUnknown": "Ismeretlen évad", @@ -56,7 +56,7 @@ "NotificationOptionPluginUninstalled": "Bővítmény eltávolítva", "NotificationOptionPluginUpdateInstalled": "Bővítményfrissítés telepítve", "NotificationOptionServerRestartRequired": "A kiszolgáló újraindítása szükséges", - "NotificationOptionTaskFailed": "Ütemezett feladat hiba", + "NotificationOptionTaskFailed": "Hiba az ütemezett feladatban", "NotificationOptionUserLockedOut": "Felhasználó tiltva", "NotificationOptionVideoPlayback": "Videólejátszás elkezdve", "NotificationOptionVideoPlaybackStopped": "Videólejátszás leállítva", @@ -107,7 +107,7 @@ "TaskCleanCache": "Gyorsítótár könyvtárának ürítése", "TasksChannelsCategory": "Internetes csatornák", "TasksApplicationCategory": "Alkalmazás", - "TasksLibraryCategory": "Könyvtár", + "TasksLibraryCategory": "Médiatár", "TasksMaintenanceCategory": "Karbantartás", "TaskDownloadMissingSubtitlesDescription": "A metaadat-konfiguráció alapján ellenőrzi és letölti a hiányzó feliratokat az internetről.", "TaskDownloadMissingSubtitles": "Hiányzó feliratok letöltése", @@ -119,19 +119,22 @@ "Undefined": "Meghatározatlan", "Forced": "Kényszerített", "Default": "Alapértelmezett", - "TaskOptimizeDatabaseDescription": "Tömöríti az adatbázist és csonkolja a szabad helyet. A feladat futtatása a könyvtár beolvasása után, vagy egyéb, adatbázis-módosítást igénylő változtatások végrehajtása javíthatja a teljesítményt.", + "TaskOptimizeDatabaseDescription": "Tömöríti az adatbázist és csonkolja a szabad helyet. A feladat futtatása a médiatár beolvasása, vagy egyéb adatbázis-módosítást igénylő változtatás végrehajtása után, javíthatja a teljesítményt.", "TaskOptimizeDatabase": "Adatbázis optimalizálása", "TaskKeyframeExtractor": "Kulcsképkockák kibontása", "TaskKeyframeExtractorDescription": "Kibontja a kulcsképkockákat a videófájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.", "External": "Külső", "HearingImpaired": "Hallássérült", - "TaskRefreshTrickplayImages": "Trickplay képek generálása", + "TaskRefreshTrickplayImages": "Trickplay képek előállítása", "TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz.", - "TaskAudioNormalization": "Hangerő Normalizáció", + "TaskAudioNormalization": "Hangerő-normalizálás", "TaskCleanCollectionsAndPlaylistsDescription": "Nem létező elemek törlése a gyűjteményekből és lejátszási listákról.", - "TaskAudioNormalizationDescription": "Hangerő normalizációs adatok keresése.", + "TaskAudioNormalizationDescription": "Hangerő-normalizálási adatok keresése.", "TaskCleanCollectionsAndPlaylists": "Gyűjtemények és lejátszási listák optimalizálása", - "TaskExtractMediaSegments": "Média szegmens felismerése", + "TaskExtractMediaSegments": "Médiaszegmens felismerése", "TaskDownloadMissingLyrics": "Hiányzó szöveg letöltése", - "TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése" + "TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése", + "TaskMoveTrickplayImages": "Trickplay képek helyének átköltöztetése", + "TaskMoveTrickplayImagesDescription": "A médiatár-beállításoknak megfelelően áthelyezi a meglévő trickplay fájlokat.", + "TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből." } From 03aa37731b39135e3b3be3f8751f3018d7427d10 Mon Sep 17 00:00:00 2001 From: Brian Howe <30811239+bhowe34@users.noreply.github.com> Date: Thu, 19 Sep 2024 08:12:32 -0500 Subject: [PATCH 087/159] Watch library directories with perm errors (#10684) --- Emby.Server.Implementations/IO/LibraryMonitor.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 31617d1a5f..6af2a553d6 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -314,6 +314,12 @@ namespace Emby.Server.Implementations.IO var ex = e.GetException(); var dw = (FileSystemWatcher)sender; + if (ex is UnauthorizedAccessException unauthorizedAccessException) + { + _logger.LogError(unauthorizedAccessException, "Permission error for Directory watcher: {Path}", dw.Path); + return; + } + _logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path); DisposeWatcher(dw, true); From 93db8990d951649c1d25003e2859776ac80e7440 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Thu, 19 Sep 2024 21:14:18 +0800 Subject: [PATCH 088/159] Enable HEVC RExt HW decoding for 4:2:2/4:4:4 content (#12664) --- .../MediaEncoding/EncodingHelper.cs | 175 ++++++++++++++---- .../Configuration/EncodingOptions.cs | 12 ++ 2 files changed, 147 insertions(+), 40 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 88aa888a1e..e26bcf21e0 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -299,7 +299,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (state.VideoStream is null || !options.EnableTonemapping - || GetVideoColorBitDepth(state) != 10 + || GetVideoColorBitDepth(state) < 10 || !_mediaEncoder.SupportsFilter("tonemapx")) { return false; @@ -312,7 +312,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (state.VideoStream is null || !options.EnableTonemapping - || GetVideoColorBitDepth(state) != 10) + || GetVideoColorBitDepth(state) < 10) { return false; } @@ -354,7 +354,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (state.VideoStream is null || !options.EnableVppTonemapping - || GetVideoColorBitDepth(state) != 10) + || GetVideoColorBitDepth(state) < 10) { return false; } @@ -377,7 +377,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (state.VideoStream is null || !options.EnableVideoToolboxTonemapping - || GetVideoColorBitDepth(state) != 10) + || GetVideoColorBitDepth(state) < 10) { return false; } @@ -388,6 +388,25 @@ namespace MediaBrowser.Controller.MediaEncoding && state.VideoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG or VideoRangeType.HDR10Plus or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG; } + private bool IsVideoStreamHevcRext(EncodingJobInfo state) + { + var videoStream = state.VideoStream; + if (videoStream is null) + { + return false; + } + + return string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase) + && (string.Equals(videoStream.Profile, "Rext", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv420p12le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv422p", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv422p10le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv422p12le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv444p10le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv444p12le", StringComparison.OrdinalIgnoreCase)); + } + /// /// Gets the name of the output video codec. /// @@ -3659,7 +3678,8 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add($"transpose_cuda=dir={tranposeDir}"); } - var outFormat = doCuTonemap ? string.Empty : "yuv420p"; + var isRext = IsVideoStreamHevcRext(state); + var outFormat = doCuTonemap ? (isRext ? "p010" : string.Empty) : "yuv420p"; var hwScaleFilter = GetHwScaleFilter("scale", "cuda", outFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); // hw scale mainFilters.Add(hwScaleFilter); @@ -4091,6 +4111,8 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (isD3d11vaDecoder || isQsvDecoder) { + var isRext = IsVideoStreamHevcRext(state); + var twoPassVppTonemap = isRext; var doVppProcamp = false; var procampParams = string.Empty; if (doVppTonemap) @@ -4100,21 +4122,21 @@ namespace MediaBrowser.Controller.MediaEncoding && options.VppTonemappingBrightness <= 100) { procampParams += $":brightness={options.VppTonemappingBrightness}"; - doVppProcamp = true; + twoPassVppTonemap = doVppProcamp = true; } if (options.VppTonemappingContrast > 1 && options.VppTonemappingContrast <= 10) { procampParams += $":contrast={options.VppTonemappingContrast}"; - doVppProcamp = true; + twoPassVppTonemap = doVppProcamp = true; } procampParams += doVppProcamp ? ":procamp=1:async_depth=2" : string.Empty; } - var outFormat = doOclTonemap ? (doVppTranspose ? "p010" : string.Empty) : "nv12"; - outFormat = (doVppTonemap && doVppProcamp) ? "p010" : outFormat; + var outFormat = doOclTonemap ? ((doVppTranspose || isRext) ? "p010" : string.Empty) : "nv12"; + outFormat = twoPassVppTonemap ? "p010" : outFormat; var swapOutputWandH = doVppTranspose && swapWAndH; var hwScalePrefix = (doVppTranspose || doVppTonemap) ? "vpp" : "scale"; @@ -4127,7 +4149,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTonemap) { - hwScaleFilter += doVppProcamp ? procampParams : ":tonemap=1"; + hwScaleFilter += doVppProcamp ? procampParams : (twoPassVppTonemap ? string.Empty : ":tonemap=1"); } if (isD3d11vaDecoder) @@ -4151,7 +4173,7 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add(hwScaleFilter); // hw tonemap(w/ procamp) - if (doVppTonemap && doVppProcamp) + if (doVppTonemap && twoPassVppTonemap) { mainFilters.Add("vpp_qsv=tonemap=1:format=nv12:async_depth=2"); } @@ -4346,6 +4368,7 @@ namespace MediaBrowser.Controller.MediaEncoding else if (isVaapiDecoder || isQsvDecoder) { var hwFilterSuffix = isVaapiDecoder ? "vaapi" : "qsv"; + var isRext = IsVideoStreamHevcRext(state); // INPUT vaapi/qsv surface(vram) // hw deint @@ -4362,6 +4385,8 @@ namespace MediaBrowser.Controller.MediaEncoding } var outFormat = doOclTonemap ? ((isQsvDecoder && doVppTranspose) ? "p010" : string.Empty) : "nv12"; + outFormat = (doTonemap && isRext) ? "p010" : outFormat; + var swapOutputWandH = isQsvDecoder && doVppTranspose && swapWAndH; var hwScalePrefix = (isQsvDecoder && doVppTranspose) ? "vpp" : "scale"; var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, hwFilterSuffix, outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); @@ -4658,6 +4683,8 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (isVaapiDecoder) { + var isRext = IsVideoStreamHevcRext(state); + // INPUT vaapi surface(vram) // hw deint if (doDeintH2645) @@ -4672,7 +4699,7 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add($"transpose_vaapi=dir={tranposeDir}"); } - var outFormat = doTonemap ? string.Empty : "nv12"; + var outFormat = doTonemap ? (isRext ? "p010" : string.Empty) : "nv12"; var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", outFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); // allocate extra pool sizes for vaapi vpp @@ -5974,7 +6001,11 @@ namespace MediaBrowser.Controller.MediaEncoding var decoderName = decoderPrefix + '_' + decoderSuffix; var isCodecAvailable = _mediaEncoder.SupportsDecoder(decoderName) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparison.OrdinalIgnoreCase); - if (bitDepth == 10 && isCodecAvailable) + + // VideoToolbox decoders have built-in SW fallback + if (bitDepth == 10 + && isCodecAvailable + && (options.HardwareAccelerationType != HardwareAccelerationType.videotoolbox)) { if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase) && options.HardwareDecodingCodecs.Contains("hevc", StringComparison.OrdinalIgnoreCase) @@ -6050,17 +6081,40 @@ namespace MediaBrowser.Controller.MediaEncoding && ffmpegVersion >= _minFFmpegDisplayRotationOption; var stripRotationDataArgs = stripRotationData ? " -display_rotation 0" : string.Empty; - if (bitDepth == 10 && isCodecAvailable) + // VideoToolbox decoders have built-in SW fallback + if (isCodecAvailable + && (options.HardwareAccelerationType != HardwareAccelerationType.videotoolbox)) { if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase) - && options.HardwareDecodingCodecs.Contains("hevc", StringComparison.OrdinalIgnoreCase) - && !options.EnableDecodingColorDepth10Hevc) + && options.HardwareDecodingCodecs.Contains("hevc", StringComparison.OrdinalIgnoreCase)) { - return null; + if (IsVideoStreamHevcRext(state)) + { + if (bitDepth <= 10 && !options.EnableDecodingColorDepth10HevcRext) + { + return null; + } + + if (bitDepth == 12 && !options.EnableDecodingColorDepth12HevcRext) + { + return null; + } + + if (hardwareAccelerationType == HardwareAccelerationType.vaapi + && !_mediaEncoder.IsVaapiDeviceInteliHD) + { + return null; + } + } + else if (bitDepth == 10 && !options.EnableDecodingColorDepth10Hevc) + { + return null; + } } if (string.Equals(videoCodec, "vp9", StringComparison.OrdinalIgnoreCase) && options.HardwareDecodingCodecs.Contains("vp9", StringComparison.OrdinalIgnoreCase) + && bitDepth == 10 && !options.EnableDecodingColorDepth10Vp9) { return null; @@ -6172,6 +6226,14 @@ namespace MediaBrowser.Controller.MediaEncoding var is8bitSwFormatsQsv = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsQsv = is8bitSwFormatsQsv || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); + var is8_10_12bitSwFormatsQsv = is8_10bitSwFormatsQsv + || string.Equals("yuv422p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); // TODO: add more 8/10bit and 4:4:4 formats for Qsv after finishing the ffcheck tool if (is8bitSwFormatsQsv) @@ -6200,12 +6262,6 @@ namespace MediaBrowser.Controller.MediaEncoding if (is8_10bitSwFormatsQsv) { - if (string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase)) - { - return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "qsv", "hevc", bitDepth); - } - if (string.Equals(videoStream.Codec, "vp9", StringComparison.OrdinalIgnoreCase)) { return GetHwaccelType(state, options, "vp9", bitDepth, hwSurface) + GetHwDecoderName(options, "vp9", "qsv", "vp9", bitDepth); @@ -6217,6 +6273,15 @@ namespace MediaBrowser.Controller.MediaEncoding } } + if (is8_10_12bitSwFormatsQsv) + { + if (string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase)) + { + return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "qsv", "hevc", bitDepth); + } + } + return null; } @@ -6232,6 +6297,11 @@ namespace MediaBrowser.Controller.MediaEncoding var is8bitSwFormatsNvdec = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsNvdec = is8bitSwFormatsNvdec || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); + var is8_10_12bitSwFormatsNvdec = is8_10bitSwFormatsNvdec + || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); // TODO: add more 8/10/12bit and 4:4:4 formats for Nvdec after finishing the ffcheck tool if (is8bitSwFormatsNvdec) @@ -6265,12 +6335,6 @@ namespace MediaBrowser.Controller.MediaEncoding if (is8_10bitSwFormatsNvdec) { - if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) - || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) - { - return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "cuvid", "hevc", bitDepth); - } - if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) { return GetHwaccelType(state, options, "vp9", bitDepth, hwSurface) + GetHwDecoderName(options, "vp9", "cuvid", "vp9", bitDepth); @@ -6282,6 +6346,15 @@ namespace MediaBrowser.Controller.MediaEncoding } } + if (is8_10_12bitSwFormatsNvdec) + { + if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) + || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) + { + return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "cuvid", "hevc", bitDepth); + } + } + return null; } @@ -6356,6 +6429,14 @@ namespace MediaBrowser.Controller.MediaEncoding var is8bitSwFormatsVaapi = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsVaapi = is8bitSwFormatsVaapi || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); + var is8_10_12bitSwFormatsVaapi = is8_10bitSwFormatsVaapi + || string.Equals("yuv422p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); if (is8bitSwFormatsVaapi) { @@ -6383,12 +6464,6 @@ namespace MediaBrowser.Controller.MediaEncoding if (is8_10bitSwFormatsVaapi) { - if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) - || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) - { - return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface); - } - if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) { return GetHwaccelType(state, options, "vp9", bitDepth, hwSurface); @@ -6400,6 +6475,15 @@ namespace MediaBrowser.Controller.MediaEncoding } } + if (is8_10_12bitSwFormatsVaapi) + { + if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) + || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) + { + return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface); + } + } + return null; } @@ -6414,6 +6498,14 @@ namespace MediaBrowser.Controller.MediaEncoding var is8bitSwFormatsVt = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsVt = is8bitSwFormatsVt || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); + var is8_10_12bitSwFormatsVt = is8_10bitSwFormatsVt + || string.Equals("yuv422p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); // The related patches make videotoolbox hardware surface working is only available in jellyfin-ffmpeg 7.0.1 at the moment. bool useHwSurface = (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface) && IsVideoToolboxFullSupported(); @@ -6434,16 +6526,19 @@ namespace MediaBrowser.Controller.MediaEncoding return GetHwaccelType(state, options, "h264", bitDepth, useHwSurface); } + if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) + { + return GetHwaccelType(state, options, "vp9", bitDepth, useHwSurface); + } + } + + if (is8_10_12bitSwFormatsVt) + { if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) { return GetHwaccelType(state, options, "hevc", bitDepth, useHwSurface); } - - if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) - { - return GetHwaccelType(state, options, "vp9", bitDepth, useHwSurface); - } } return null; diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index d67a2479fb..2720c0bdf6 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -46,6 +46,8 @@ public class EncodingOptions DeinterlaceMethod = DeinterlaceMethod.yadif; EnableDecodingColorDepth10Hevc = true; EnableDecodingColorDepth10Vp9 = true; + EnableDecodingColorDepth10HevcRext = false; + EnableDecodingColorDepth12HevcRext = false; // Enhanced Nvdec or system native decoder is required for DoVi to SDR tone-mapping. EnableEnhancedNvdecDecoder = true; PreferSystemNativeHwDecoder = true; @@ -234,6 +236,16 @@ public class EncodingOptions /// public bool EnableDecodingColorDepth10Vp9 { get; set; } + /// + /// Gets or sets a value indicating whether 8/10bit HEVC RExt decoding is enabled. + /// + public bool EnableDecodingColorDepth10HevcRext { get; set; } + + /// + /// Gets or sets a value indicating whether 12bit HEVC RExt decoding is enabled. + /// + public bool EnableDecodingColorDepth12HevcRext { get; set; } + /// /// Gets or sets a value indicating whether the enhanced NVDEC is enabled. /// From d4bde14a0143ae60db5187084dbb8cbf93e77cfd Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Thu, 19 Sep 2024 16:46:59 +0200 Subject: [PATCH 089/159] Update src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs Co-authored-by: Bond-009 --- .../Json/Converters/JsonDelimitedArrayConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs index b9477ce6b7..936a5a97c4 100644 --- a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs +++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs @@ -57,7 +57,7 @@ namespace Jellyfin.Extensions.Json.Converters } } - return [.. typedValues]; + return typedValues.ToArray(); } return JsonSerializer.Deserialize(ref reader, options); From 2c0520b5401620bc121e1e4424e3b4a3f08656f2 Mon Sep 17 00:00:00 2001 From: Bas <44002186+854562@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:41:47 +0000 Subject: [PATCH 090/159] Translated using Weblate (Dutch) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/ --- .../Localization/Core/nl.json | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 1522720dc5..7d101195b7 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -16,13 +16,13 @@ "Folders": "Mappen", "Genres": "Genres", "HeaderAlbumArtists": "Albumartiesten", - "HeaderContinueWatching": "Kijken hervatten", + "HeaderContinueWatching": "Verderkijken", "HeaderFavoriteAlbums": "Favoriete albums", "HeaderFavoriteArtists": "Favoriete artiesten", "HeaderFavoriteEpisodes": "Favoriete afleveringen", - "HeaderFavoriteShows": "Favoriete shows", + "HeaderFavoriteShows": "Favoriete series", "HeaderFavoriteSongs": "Favoriete nummers", - "HeaderLiveTV": "Live TV", + "HeaderLiveTV": "Live-tv", "HeaderNextUp": "Volgende", "HeaderRecordingGroups": "Opnamegroepen", "HomeVideos": "Homevideo's", @@ -34,8 +34,8 @@ "Latest": "Nieuwste", "MessageApplicationUpdated": "Jellyfin Server is bijgewerkt", "MessageApplicationUpdatedTo": "Jellyfin Server is bijgewerkt naar {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Sectie {0} van de server configuratie is bijgewerkt", - "MessageServerConfigurationUpdated": "Server configuratie is bijgewerkt", + "MessageNamedServerConfigurationUpdatedWithValue": "Sectie {0} van de serverconfiguratie is bijgewerkt", + "MessageServerConfigurationUpdated": "Serverconfiguratie is bijgewerkt", "MixedContent": "Gemengde inhoud", "Movies": "Films", "Music": "Muziek", @@ -50,12 +50,12 @@ "NotificationOptionAudioPlaybackStopped": "Muziek gestopt", "NotificationOptionCameraImageUploaded": "Camera-afbeelding geüpload", "NotificationOptionInstallationFailed": "Installatie mislukt", - "NotificationOptionNewLibraryContent": "Nieuwe content toegevoegd", - "NotificationOptionPluginError": "Plug-in fout", + "NotificationOptionNewLibraryContent": "Nieuwe inhoud toegevoegd", + "NotificationOptionPluginError": "Plug-in-fout", "NotificationOptionPluginInstalled": "Plug-in geïnstalleerd", "NotificationOptionPluginUninstalled": "Plug-in verwijderd", "NotificationOptionPluginUpdateInstalled": "Plug-in-update geïnstalleerd", - "NotificationOptionServerRestartRequired": "Server herstart nodig", + "NotificationOptionServerRestartRequired": "Herstarten server vereist", "NotificationOptionTaskFailed": "Geplande taak mislukt", "NotificationOptionUserLockedOut": "Gebruiker is vergrendeld", "NotificationOptionVideoPlayback": "Afspelen van video gestart", @@ -72,16 +72,16 @@ "ServerNameNeedsToBeRestarted": "{0} moet herstart worden", "Shows": "Series", "Songs": "Nummers", - "StartupEmbyServerIsLoading": "Jellyfin Server is aan het laden, probeer het later opnieuw.", + "StartupEmbyServerIsLoading": "Jellyfin Server is aan het laden. Probeer het later opnieuw.", "SubtitleDownloadFailureForItem": "Downloaden van ondertiteling voor {0} is mislukt", - "SubtitleDownloadFailureFromForItem": "Ondertitels konden niet gedownload worden van {0} voor {1}", + "SubtitleDownloadFailureFromForItem": "Ondertiteling kon niet gedownload worden van {0} voor {1}", "Sync": "Synchronisatie", "System": "Systeem", "TvShows": "TV-series", "User": "Gebruiker", "UserCreatedWithName": "Gebruiker {0} is aangemaakt", "UserDeletedWithName": "Gebruiker {0} is verwijderd", - "UserDownloadingItemWithValues": "{0} download {1}", + "UserDownloadingItemWithValues": "{0} downloadt {1}", "UserLockedOutWithName": "Gebruikersaccount {0} is vergrendeld", "UserOfflineFromDevice": "Verbinding van {0} met {1} is verbroken", "UserOnlineFromDevice": "{0} heeft verbinding met {1}", @@ -90,7 +90,7 @@ "UserStartedPlayingItemWithValues": "{0} speelt {1} af op {2}", "UserStoppedPlayingItemWithValues": "{0} heeft afspelen van {1} gestopt op {2}", "ValueHasBeenAddedToLibrary": "{0} is toegevoegd aan je mediabibliotheek", - "ValueSpecialEpisodeName": "Speciaal - {0}", + "ValueSpecialEpisodeName": "Special - {0}", "VersionNumber": "Versie {0}", "TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar ontbrekende ondertiteling gebaseerd op metadataconfiguratie.", "TaskDownloadMissingSubtitles": "Ontbrekende ondertiteling downloaden", From 7ab7f69916a7c207f290e5e931a6c7749dd4413b Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Fri, 20 Sep 2024 03:22:53 +0800 Subject: [PATCH 091/159] Enable key-frame only decoding for RKMPP trickplay Signed-off-by: nyanmisaka --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 6f87692521..517d135d3b 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -804,7 +804,8 @@ namespace MediaBrowser.MediaEncoding.Encoder || (hardwareAccelerationType == HardwareAccelerationType.amf && OperatingSystem.IsWindows()) || (hardwareAccelerationType == HardwareAccelerationType.qsv && options.PreferSystemNativeHwDecoder) || hardwareAccelerationType == HardwareAccelerationType.vaapi - || hardwareAccelerationType == HardwareAccelerationType.videotoolbox; + || hardwareAccelerationType == HardwareAccelerationType.videotoolbox + || hardwareAccelerationType == HardwareAccelerationType.rkmpp; if (!supportsKeyFrameOnly) { // Disable hardware acceleration when the hardware decoder does not support keyframe only mode. @@ -930,13 +931,14 @@ namespace MediaBrowser.MediaEncoding.Encoder // Final command arguments var args = string.Format( CultureInfo.InvariantCulture, - "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}{5}-f {6} \"{7}\"", + "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}{5}{6}-f {7} \"{8}\"", inputArg, filterParam, outputThreads.GetValueOrDefault(_threads), vidEncoder, encoderQualityOption + encoderQuality + " ", vidEncoder.Contains("videotoolbox", StringComparison.InvariantCultureIgnoreCase) ? "-allow_sw 1 " : string.Empty, // allow_sw fallback for some intel macs + EncoderVersion >= new Version(5, 1) ? "-fps_mode passthrough " : "-vsync passthrough ", // passthrough timestamp "image2", outputPath); From e615b56a70d88c441589bb9f1fd0159e97ea4b1a Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Fri, 20 Sep 2024 21:01:58 +0800 Subject: [PATCH 092/159] Fix RKMPP 2pass scaling in Trickplay (#12675) --- .../MediaEncoding/EncodingHelper.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index e26bcf21e0..d8c4f2f850 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -5570,13 +5570,22 @@ namespace MediaBrowser.Controller.MediaEncoding var outFormat = doOclTonemap ? "p010" : "nv12"; 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); + var doScaling = GetHwScaleFilter(hwScalePrefix, "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); if (!hasSubs || doRkVppTranspose || !isFullAfbcPipeline - || !string.IsNullOrEmpty(hwScaleFilter2)) + || !string.IsNullOrEmpty(doScaling)) { + // RGA3 hardware only support (1/8 ~ 8) scaling in each blit operation, + // but in Trickplay there's a case: (3840/320 == 12), enable 2pass for it + if (!string.IsNullOrEmpty(doScaling) + && !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f)) + { + var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/8:h=ih/8:format={outFormat}:afbc=1"; + mainFilters.Add(hwScaleFilterFirstPass); + } + if (!string.IsNullOrEmpty(hwScaleFilter) && doRkVppTranspose) { hwScaleFilter += $":transpose={tranposeDir}"; From 41fb696ef6801af786ae7f6f3fe1c7a10121114b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Sep 2024 07:46:31 -0600 Subject: [PATCH 093/159] Update github/codeql-action action to v3.26.8 (#12672) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index de5503b843..551dbae987 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '8.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 + uses: github/codeql-action/init@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 + uses: github/codeql-action/autobuild@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 + uses: github/codeql-action/analyze@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 From 9ff7575c85d05714a497502f95ee38f6f3b87752 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Fri, 20 Sep 2024 15:46:44 +0200 Subject: [PATCH 094/159] Fix metadata merge for BoxSets (#12583) --- MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs index df9d15ec20..32ab7716f7 100644 --- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs +++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs @@ -54,7 +54,14 @@ namespace MediaBrowser.Providers.BoxSets if (mergeMetadataSettings) { - targetItem.LinkedChildren = sourceItem.LinkedChildren; + if (replaceData || targetItem.LinkedChildren.Length == 0) + { + targetItem.LinkedChildren = sourceItem.LinkedChildren; + } + else + { + targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray(); + } } } From 78638c72fb2fd15e7c9277bb37f5a6a93b9cc0a9 Mon Sep 17 00:00:00 2001 From: l00d3r Date: Sat, 21 Sep 2024 11:52:24 +0000 Subject: [PATCH 095/159] Translated using Weblate (Estonian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/et/ --- Emby.Server.Implementations/Localization/Core/et.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index 075bcc9a44..fcb12718a0 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -102,7 +102,7 @@ "Forced": "Sunnitud", "Folders": "Kaustad", "Favorites": "Lemmikud", - "FailedLoginAttemptWithUserName": "{0} - sisselogimine nurjus", + "FailedLoginAttemptWithUserName": "Sisselogimine nurjus aadressilt {0}", "DeviceOnlineWithName": "{0} on ühendatud", "DeviceOfflineWithName": "{0} katkestas ühenduse", "Default": "Vaikimisi", From 092e9e29f1d94a8b6a88868d56c5b4801cbe5e35 Mon Sep 17 00:00:00 2001 From: l00d3r Date: Sat, 21 Sep 2024 12:32:53 +0000 Subject: [PATCH 096/159] Translated using Weblate (Estonian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/et/ --- Emby.Server.Implementations/Localization/Core/et.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index fcb12718a0..7ca4e431d9 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -129,5 +129,7 @@ "TaskAudioNormalization": "Heli Normaliseerimine", "TaskAudioNormalizationDescription": "Skaneerib faile heli normaliseerimise andmete jaoks.", "TaskCleanCollectionsAndPlaylistsDescription": "Eemaldab kogumikest ja esitusloenditest asjad, mida enam ei eksisteeri.", - "TaskCleanCollectionsAndPlaylists": "Puhasta kogumikud ja esitusloendid" + "TaskCleanCollectionsAndPlaylists": "Puhasta kogumikud ja esitusloendid", + "TaskDownloadMissingLyrics": "Lae alla puuduolev lüürika", + "TaskDownloadMissingLyricsDescription": "Lae lauludele alla lüürika" } From 38f80edc8011783d6544a9c1bd6401e3fe13ff57 Mon Sep 17 00:00:00 2001 From: l00d3r Date: Sat, 21 Sep 2024 12:37:52 +0000 Subject: [PATCH 097/159] Translated using Weblate (Estonian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/et/ --- Emby.Server.Implementations/Localization/Core/et.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index 7ca4e431d9..3b2bb70a95 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -131,5 +131,9 @@ "TaskCleanCollectionsAndPlaylistsDescription": "Eemaldab kogumikest ja esitusloenditest asjad, mida enam ei eksisteeri.", "TaskCleanCollectionsAndPlaylists": "Puhasta kogumikud ja esitusloendid", "TaskDownloadMissingLyrics": "Lae alla puuduolev lüürika", - "TaskDownloadMissingLyricsDescription": "Lae lauludele alla lüürika" + "TaskDownloadMissingLyricsDescription": "Lae lauludele alla lüürika", + "TaskMoveTrickplayImagesDescription": "Liigutab trickplay pildid meediakogu sätete kohaselt.", + "TaskExtractMediaSegments": "Meediasegmentide skaneerimine", + "TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.", + "TaskMoveTrickplayImages": "Migreeri trickplay piltide asukoht" } From d944f415f3cc0e5433d94b11a16684ca3f0131ec Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 22 Sep 2024 00:34:47 +0800 Subject: [PATCH 098/159] Let HLS Controller decide if subtitle should be burn in Previously, we predicted whether the subtitle should be burned in with transcode reasons, but that was not accurate because the actual transcoding codec is only determined after the client has requested the stream. This pass through the option to the `DynamicHlsController` to handle the subtitle burn-in during the actual transcoding process. Now the client should be responsible to conditionally load the subtitle when this option is enabled. --- .../Controllers/DynamicHlsController.cs | 63 ++++++++++++------- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 6 ++ .../MediaEncoding/BaseEncodingJobOptions.cs | 2 + .../MediaEncoding/EncodingHelper.cs | 38 ++++++----- MediaBrowser.Model/Dlna/StreamBuilder.cs | 18 +----- MediaBrowser.Model/Dlna/StreamInfo.cs | 7 ++- 6 files changed, 81 insertions(+), 53 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index db1d866985..924f010e40 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -158,6 +158,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// Optional. The max height. /// Optional. Whether to enable subtitles in the manifest. /// Optional. Whether to enable Audio Encoding. + /// Whether to always burn in subtitles when transcoding. /// Hls live stream retrieved. /// A containing the hls file. [HttpGet("Videos/{itemId}/live.m3u8")] @@ -216,7 +217,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] bool? enableSubtitlesInManifest, - [FromQuery] bool enableAudioVbrEncoding = true) + [FromQuery] bool enableAudioVbrEncoding = true, + [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) { VideoRequestDto streamingRequest = new VideoRequestDto { @@ -251,7 +253,7 @@ public class DynamicHlsController : BaseJellyfinApiController Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, RequireAvc = requireAvc ?? false, @@ -271,7 +273,8 @@ public class DynamicHlsController : BaseJellyfinApiController MaxHeight = maxHeight, MaxWidth = maxWidth, EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true, - EnableAudioVbrEncoding = enableAudioVbrEncoding + EnableAudioVbrEncoding = enableAudioVbrEncoding, + AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding }; // CTS lifecycle is managed internally. @@ -398,6 +401,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// Enable adaptive bitrate streaming. /// Enable trickplay image playlists being added to master playlist. /// Whether to enable Audio Encoding. + /// Whether to always burn in subtitles when transcoding. /// Video stream returned. /// A containing the playlist file. [HttpGet("Videos/{itemId}/master.m3u8")] @@ -457,7 +461,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] Dictionary streamOptions, [FromQuery] bool enableAdaptiveBitrateStreaming = true, [FromQuery] bool enableTrickplay = true, - [FromQuery] bool enableAudioVbrEncoding = true) + [FromQuery] bool enableAudioVbrEncoding = true, + [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) { var streamingRequest = new HlsVideoRequestDto { @@ -493,7 +498,7 @@ public class DynamicHlsController : BaseJellyfinApiController MaxHeight = maxHeight, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, RequireAvc = requireAvc ?? false, @@ -512,7 +517,8 @@ public class DynamicHlsController : BaseJellyfinApiController StreamOptions = streamOptions, EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming, EnableTrickplay = enableTrickplay, - EnableAudioVbrEncoding = enableAudioVbrEncoding + EnableAudioVbrEncoding = enableAudioVbrEncoding, + AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding }; return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); @@ -572,6 +578,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// Optional. The streaming options. /// Enable adaptive bitrate streaming. /// Optional. Whether to enable Audio Encoding. + /// Whether to always burn in subtitles when transcoding. /// Audio stream returned. /// A containing the playlist file. [HttpGet("Audio/{itemId}/master.m3u8")] @@ -629,7 +636,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] EncodingContext? context, [FromQuery] Dictionary streamOptions, [FromQuery] bool enableAdaptiveBitrateStreaming = true, - [FromQuery] bool enableAudioVbrEncoding = true) + [FromQuery] bool enableAudioVbrEncoding = true, + [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) { var streamingRequest = new HlsAudioRequestDto { @@ -663,7 +671,7 @@ public class DynamicHlsController : BaseJellyfinApiController Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, RequireAvc = requireAvc ?? false, @@ -681,7 +689,8 @@ public class DynamicHlsController : BaseJellyfinApiController Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming, - EnableAudioVbrEncoding = enableAudioVbrEncoding + EnableAudioVbrEncoding = enableAudioVbrEncoding, + AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding }; return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); @@ -741,6 +750,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// Optional. The . /// Optional. The streaming options. /// Optional. Whether to enable Audio Encoding. + /// Whether to always burn in subtitles when transcoding. /// Video stream returned. /// A containing the audio file. [HttpGet("Videos/{itemId}/main.m3u8")] @@ -797,7 +807,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, [FromQuery] Dictionary streamOptions, - [FromQuery] bool enableAudioVbrEncoding = true) + [FromQuery] bool enableAudioVbrEncoding = true, + [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) { using var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new VideoRequestDto @@ -834,7 +845,7 @@ public class DynamicHlsController : BaseJellyfinApiController MaxHeight = maxHeight, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, RequireAvc = requireAvc ?? false, @@ -851,7 +862,8 @@ public class DynamicHlsController : BaseJellyfinApiController VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, - EnableAudioVbrEncoding = enableAudioVbrEncoding + EnableAudioVbrEncoding = enableAudioVbrEncoding, + AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding }; return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource) @@ -911,6 +923,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// Optional. The . /// Optional. The streaming options. /// Optional. Whether to enable Audio Encoding. + /// Whether to always burn in subtitles when transcoding. /// Audio stream returned. /// A containing the audio file. [HttpGet("Audio/{itemId}/main.m3u8")] @@ -966,7 +979,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, [FromQuery] Dictionary streamOptions, - [FromQuery] bool enableAudioVbrEncoding = true) + [FromQuery] bool enableAudioVbrEncoding = true, + [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) { using var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new StreamingRequestDto @@ -1001,7 +1015,7 @@ public class DynamicHlsController : BaseJellyfinApiController Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, RequireAvc = requireAvc ?? false, @@ -1018,7 +1032,8 @@ public class DynamicHlsController : BaseJellyfinApiController VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, - EnableAudioVbrEncoding = enableAudioVbrEncoding + EnableAudioVbrEncoding = enableAudioVbrEncoding, + AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding }; return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource) @@ -1084,6 +1099,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// Optional. The . /// Optional. The streaming options. /// Optional. Whether to enable Audio Encoding. + /// Whether to always burn in subtitles when transcoding. /// Video stream returned. /// A containing the audio file. [HttpGet("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] @@ -1146,7 +1162,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, [FromQuery] Dictionary streamOptions, - [FromQuery] bool enableAudioVbrEncoding = true) + [FromQuery] bool enableAudioVbrEncoding = true, + [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) { var streamingRequest = new VideoRequestDto { @@ -1185,7 +1202,7 @@ public class DynamicHlsController : BaseJellyfinApiController MaxHeight = maxHeight, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, RequireAvc = requireAvc ?? false, @@ -1202,7 +1219,8 @@ public class DynamicHlsController : BaseJellyfinApiController VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, - EnableAudioVbrEncoding = enableAudioVbrEncoding + EnableAudioVbrEncoding = enableAudioVbrEncoding, + AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding }; return await GetDynamicSegment(streamingRequest, segmentId) @@ -1267,6 +1285,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// Optional. The . /// Optional. The streaming options. /// Optional. Whether to enable Audio Encoding. + /// Whether to always burn in subtitles when transcoding. /// Video stream returned. /// A containing the audio file. [HttpGet("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] @@ -1328,7 +1347,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, [FromQuery] Dictionary streamOptions, - [FromQuery] bool enableAudioVbrEncoding = true) + [FromQuery] bool enableAudioVbrEncoding = true, + [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) { var streamingRequest = new StreamingRequestDto { @@ -1365,7 +1385,7 @@ public class DynamicHlsController : BaseJellyfinApiController Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, RequireAvc = requireAvc ?? false, @@ -1382,7 +1402,8 @@ public class DynamicHlsController : BaseJellyfinApiController VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, - EnableAudioVbrEncoding = enableAudioVbrEncoding + EnableAudioVbrEncoding = enableAudioVbrEncoding, + AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding }; return await GetDynamicSegment(streamingRequest, segmentId) diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 5050cab418..2d9ecd4f08 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -293,6 +293,7 @@ public class MediaInfoHelper mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; mediaSource.TranscodingContainer = streamInfo.Container; mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; + mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true"; } else { @@ -310,6 +311,11 @@ public class MediaInfoHelper { mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; } + + if (streamInfo.AlwaysBurnInSubtitleWhenTranscoding) + { + mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true"; + } } } diff --git a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs index f77186e25c..20f51ddb71 100644 --- a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs +++ b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs @@ -193,6 +193,8 @@ namespace MediaBrowser.Controller.MediaEncoding public bool EnableAudioVbrEncoding { get; set; } + public bool AlwaysBurnInSubtitleWhenTranscoding { get; set; } + public string GetOption(string qualifier, string name) { var value = GetOption(qualifier + "-" + name); diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index d8c4f2f850..f8ba8ddd80 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -941,7 +941,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // DVBSUB uses the fixed canvas size 720x576 if (state.SubtitleStream is not null - && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode + && ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream && !string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase)) { @@ -1240,7 +1240,7 @@ namespace MediaBrowser.Controller.MediaEncoding // sub2video for external graphical subtitles if (state.SubtitleStream is not null - && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode + && ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleStream.IsExternal) { @@ -2554,7 +2554,7 @@ namespace MediaBrowser.Controller.MediaEncoding } var isCopyingTimestamps = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive; - if (state.SubtitleStream is not null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode && !isCopyingTimestamps) + if (state.SubtitleStream is not null && state.SubtitleStream.IsTextSubtitleStream && ShouldEncodeSubtitle(state) && !isCopyingTimestamps) { var seconds = TimeSpan.FromTicks(state.StartTimeTicks ?? 0).TotalSeconds; @@ -2755,7 +2755,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.AudioStream.IsExternal) { bool hasExternalGraphicsSubs = state.SubtitleStream is not null - && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode + && ShouldEncodeSubtitle(state) && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream; int externalAudioMapIndex = hasExternalGraphicsSubs ? 2 : 1; @@ -3475,7 +3475,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doToneMap = IsSwTonemapAvailable(state, options); var requireDoviReshaping = doToneMap && state.VideoStream.VideoRangeType == VideoRangeType.DOVI; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; @@ -3618,7 +3618,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doCuTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -3824,7 +3824,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doOclTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -4064,7 +4064,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doOclTonemap = !doVppTonemap && IsHwTonemapAvailable(state, options); var doTonemap = doVppTonemap || doOclTonemap; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -4320,7 +4320,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doTonemap = doVaVppTonemap || doOclTonemap; var doDeintH2645 = doDeintH264 || doDeintHevc; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -4636,7 +4636,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doTonemap = doVaVppTonemap || doOclTonemap; var doDeintH2645 = doDeintH264 || doDeintHevc; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -4858,7 +4858,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doVkTonemap = IsVulkanHwTonemapAvailable(state, options); var doDeintH2645 = doDeintH264 || doDeintHevc; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -5091,7 +5091,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doOclTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; @@ -5339,7 +5339,7 @@ namespace MediaBrowser.Controller.MediaEncoding 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 hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -5511,7 +5511,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doOclTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream != null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -5722,7 +5722,7 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Empty; } - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; @@ -7156,7 +7156,7 @@ namespace MediaBrowser.Controller.MediaEncoding args += keyFrameArg; - var hasGraphicalSubs = state.SubtitleStream is not null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasGraphicalSubs = state.SubtitleStream is not null && !state.SubtitleStream.IsTextSubtitleStream && ShouldEncodeSubtitle(state); var hasCopyTs = false; @@ -7361,5 +7361,11 @@ namespace MediaBrowser.Controller.MediaEncoding { return string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase); } + + private static bool ShouldEncodeSubtitle(EncodingJobInfo state) + { + return state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode + || (state.BaseRequest.AlwaysBurnInSubtitleWhenTranscoding && !IsCopyCodec(state.OutputVideoCodec)); + } } } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index bf122dcc7f..6c45f19468 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -639,7 +639,8 @@ namespace MediaBrowser.Model.Dlna RunTimeTicks = item.RunTimeTicks, Context = options.Context, DeviceProfile = options.Profile, - SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles) + SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles), + AlwaysBurnInSubtitleWhenTranscoding = options.AlwaysBurnInSubtitleWhenTranscoding }; var subtitleStream = playlistItem.SubtitleStreamIndex.HasValue ? item.GetMediaStream(MediaStreamType.Subtitle, playlistItem.SubtitleStreamIndex.Value) : null; @@ -767,20 +768,7 @@ namespace MediaBrowser.Model.Dlna if (subtitleStream is not null) { var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, _transcoderSupport, transcodingProfile.Container, transcodingProfile.Protocol); - - if (options.AlwaysBurnInSubtitleWhenTranscoding && (playlistItem.TranscodeReasons & (VideoReasons | TranscodeReason.ContainerBitrateExceedsLimit)) != 0) - { - playlistItem.SubtitleDeliveryMethod = SubtitleDeliveryMethod.Encode; - foreach (SubtitleProfile profile in options.Profile.SubtitleProfiles) - { - profile.Method = SubtitleDeliveryMethod.Encode; - } - } - else - { - playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method; - } - + playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method; playlistItem.SubtitleFormat = subtitleProfile.Format; playlistItem.SubtitleCodecs = [subtitleProfile.Format]; } diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 3be6860880..1ae4e1962d 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -270,6 +270,11 @@ public class StreamInfo /// public bool EnableAudioVbrEncoding { get; set; } + /// + /// Gets or sets a value indicating whether always burn in subtitles when transcoding. + /// + public bool AlwaysBurnInSubtitleWhenTranscoding { get; set; } + /// /// Gets a value indicating whether the stream is direct. /// @@ -953,7 +958,7 @@ public class StreamInfo list.Add(new NameValuePair("VideoCodec", videoCodecs)); list.Add(new NameValuePair("AudioCodec", audioCodecs)); list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && (item.AlwaysBurnInSubtitleWhenTranscoding || item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); From c3e889cd41e534b15e0930e2e76fcf12238e5f16 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 22 Sep 2024 01:11:00 +0800 Subject: [PATCH 099/159] Conditionally add burn in option for remote source --- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 2d9ecd4f08..4adda0b695 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -293,7 +293,10 @@ public class MediaInfoHelper mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; mediaSource.TranscodingContainer = streamInfo.Container; mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; - mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true"; + if (streamInfo.AlwaysBurnInSubtitleWhenTranscoding) + { + mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true"; + } } else { From 1346ebc13448e4e4e4096ac31320abf7cf3a6baa Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 22 Sep 2024 03:13:30 +0800 Subject: [PATCH 100/159] Don't add subtitle option to audio endpoint --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 924f010e40..05c9faddf3 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -578,7 +578,6 @@ public class DynamicHlsController : BaseJellyfinApiController /// Optional. The streaming options. /// Enable adaptive bitrate streaming. /// Optional. Whether to enable Audio Encoding. - /// Whether to always burn in subtitles when transcoding. /// Audio stream returned. /// A containing the playlist file. [HttpGet("Audio/{itemId}/master.m3u8")] @@ -636,8 +635,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] EncodingContext? context, [FromQuery] Dictionary streamOptions, [FromQuery] bool enableAdaptiveBitrateStreaming = true, - [FromQuery] bool enableAudioVbrEncoding = true, - [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) + [FromQuery] bool enableAudioVbrEncoding = true) { var streamingRequest = new HlsAudioRequestDto { @@ -690,7 +688,7 @@ public class DynamicHlsController : BaseJellyfinApiController StreamOptions = streamOptions, EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming, EnableAudioVbrEncoding = enableAudioVbrEncoding, - AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding + AlwaysBurnInSubtitleWhenTranscoding = false }; return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); From 4502024468ecd9a795cd482f4fbda30b35ee58de Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 22 Sep 2024 04:41:30 +0800 Subject: [PATCH 101/159] Remove all subtitle options from audio endpoints Co-authored-by: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com> --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 05c9faddf3..48fda471a6 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -921,7 +921,6 @@ public class DynamicHlsController : BaseJellyfinApiController /// Optional. The . /// Optional. The streaming options. /// Optional. Whether to enable Audio Encoding. - /// Whether to always burn in subtitles when transcoding. /// Audio stream returned. /// A containing the audio file. [HttpGet("Audio/{itemId}/main.m3u8")] @@ -977,8 +976,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, [FromQuery] Dictionary streamOptions, - [FromQuery] bool enableAudioVbrEncoding = true, - [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) + [FromQuery] bool enableAudioVbrEncoding = true) { using var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new StreamingRequestDto @@ -1031,7 +1029,7 @@ public class DynamicHlsController : BaseJellyfinApiController Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, EnableAudioVbrEncoding = enableAudioVbrEncoding, - AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding + AlwaysBurnInSubtitleWhenTranscoding = false }; return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource) @@ -1283,7 +1281,6 @@ public class DynamicHlsController : BaseJellyfinApiController /// Optional. The . /// Optional. The streaming options. /// Optional. Whether to enable Audio Encoding. - /// Whether to always burn in subtitles when transcoding. /// Video stream returned. /// A containing the audio file. [HttpGet("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] @@ -1345,8 +1342,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, [FromQuery] Dictionary streamOptions, - [FromQuery] bool enableAudioVbrEncoding = true, - [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) + [FromQuery] bool enableAudioVbrEncoding = true) { var streamingRequest = new StreamingRequestDto { @@ -1401,7 +1397,7 @@ public class DynamicHlsController : BaseJellyfinApiController Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, EnableAudioVbrEncoding = enableAudioVbrEncoding, - AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding + AlwaysBurnInSubtitleWhenTranscoding = false }; return await GetDynamicSegment(streamingRequest, segmentId) From 56cf1a581c7f0ffc19fca284a13878d231136aaa Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 22 Sep 2024 10:01:47 +0800 Subject: [PATCH 102/159] Better bitrate and resolution normalization (#12644) --- Jellyfin.Api/Helpers/StreamingHelpers.cs | 8 ++- .../MediaEncoding/EncodingHelper.cs | 8 ++- .../Dlna/ResolutionNormalizer.cs | 71 ++++++++++--------- 3 files changed, 51 insertions(+), 36 deletions(-) diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 3cc6a393bc..3a5db2f3fb 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -219,11 +219,17 @@ public static class StreamingHelpers } else { + var h264EquivalentBitrate = EncodingHelper.ScaleBitrate( + state.OutputVideoBitrate.Value, + state.ActualOutputVideoCodec, + "h264"); var resolution = ResolutionNormalizer.Normalize( state.VideoStream?.BitRate, state.OutputVideoBitrate.Value, + h264EquivalentBitrate, state.VideoRequest.MaxWidth, - state.VideoRequest.MaxHeight); + state.VideoRequest.MaxHeight, + state.TargetFramerate); state.VideoRequest.MaxWidth = resolution.MaxWidth; state.VideoRequest.MaxHeight = resolution.MaxHeight; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index f8ba8ddd80..788bd03351 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2400,7 +2400,7 @@ namespace MediaBrowser.Controller.MediaEncoding return 1; } - private static int ScaleBitrate(int bitrate, string inputVideoCodec, string outputVideoCodec) + public static int ScaleBitrate(int bitrate, string inputVideoCodec, string outputVideoCodec) { var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec); var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec); @@ -2424,6 +2424,12 @@ namespace MediaBrowser.Controller.MediaEncoding { scaleFactor = Math.Max(scaleFactor, 2); } + else if (bitrate >= 30000000) + { + // Don't scale beyond 30Mbps, it is hardly visually noticeable for most codecs with our prefer speed encoding + // and will cause extremely high bitrate to be used for av1->h264 transcoding that will overload clients and encoders + scaleFactor = 1; + } return Convert.ToInt32(scaleFactor * bitrate); } diff --git a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs index 5d7daa81aa..1a636b2403 100644 --- a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs +++ b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs @@ -2,28 +2,33 @@ #pragma warning disable CS1591 using System; +using System.Linq; namespace MediaBrowser.Model.Dlna { public static class ResolutionNormalizer { - private static readonly ResolutionConfiguration[] Configurations = - new[] - { - new ResolutionConfiguration(426, 320000), - new ResolutionConfiguration(640, 400000), - new ResolutionConfiguration(720, 950000), - new ResolutionConfiguration(1280, 2500000), - new ResolutionConfiguration(1920, 4000000), - new ResolutionConfiguration(2560, 20000000), - new ResolutionConfiguration(3840, 35000000) - }; + // Please note: all bitrate here are in the scale of SDR h264 bitrate at 30fps + private static readonly ResolutionConfiguration[] _configurations = + [ + new ResolutionConfiguration(416, 365000), + new ResolutionConfiguration(640, 730000), + new ResolutionConfiguration(768, 1100000), + new ResolutionConfiguration(960, 3000000), + new ResolutionConfiguration(1280, 6000000), + new ResolutionConfiguration(1920, 13500000), + new ResolutionConfiguration(2560, 28000000), + new ResolutionConfiguration(3840, 50000000) + ]; public static ResolutionOptions Normalize( int? inputBitrate, int outputBitrate, + int h264EquivalentOutputBitrate, int? maxWidth, - int? maxHeight) + int? maxHeight, + float? targetFps, + bool isHdr = false) // We are not doing HDR transcoding for now, leave for future use { // If the bitrate isn't changing, then don't downscale the resolution if (inputBitrate.HasValue && outputBitrate >= inputBitrate.Value) @@ -38,16 +43,26 @@ namespace MediaBrowser.Model.Dlna } } - var resolutionConfig = GetResolutionConfiguration(outputBitrate); - if (resolutionConfig is not null) - { - var originvalValue = maxWidth; + var referenceBitrate = h264EquivalentOutputBitrate * (30.0f / (targetFps ?? 30.0f)); - maxWidth = Math.Min(resolutionConfig.MaxWidth, maxWidth ?? resolutionConfig.MaxWidth); - if (!originvalValue.HasValue || originvalValue.Value != maxWidth.Value) - { - maxHeight = null; - } + if (isHdr) + { + referenceBitrate *= 0.8f; + } + + var resolutionConfig = GetResolutionConfiguration(Convert.ToInt32(referenceBitrate)); + + if (resolutionConfig is null) + { + return new ResolutionOptions { MaxWidth = maxWidth, MaxHeight = maxHeight }; + } + + var originWidthValue = maxWidth; + + maxWidth = Math.Min(resolutionConfig.MaxWidth, maxWidth ?? resolutionConfig.MaxWidth); + if (!originWidthValue.HasValue || originWidthValue.Value != maxWidth.Value) + { + maxHeight = null; } return new ResolutionOptions @@ -59,19 +74,7 @@ namespace MediaBrowser.Model.Dlna private static ResolutionConfiguration GetResolutionConfiguration(int outputBitrate) { - ResolutionConfiguration previousOption = null; - - foreach (var config in Configurations) - { - if (outputBitrate <= config.MaxBitrate) - { - return previousOption ?? config; - } - - previousOption = config; - } - - return null; + return _configurations.FirstOrDefault(config => outputBitrate <= config.MaxBitrate); } } } From 62606e46b538138d2d8c5b901344cdecc069c5c6 Mon Sep 17 00:00:00 2001 From: Nyanmisaka <799610810@qq.com> Date: Sun, 22 Sep 2024 09:56:44 +0000 Subject: [PATCH 103/159] Translated using Weblate (Chinese (Simplified Han script)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/ --- Emby.Server.Implementations/Localization/Core/zh-CN.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index cbec0979ac..a406a73b78 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -125,15 +125,15 @@ "TaskKeyframeExtractor": "关键帧提取器", "External": "外部", "HearingImpaired": "听力障碍", - "TaskRefreshTrickplayImages": "生成时间轴缩略图", - "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成时间轴缩略图。", + "TaskRefreshTrickplayImages": "生成特技播放预览图", + "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成特技播放预览图。", "TaskCleanCollectionsAndPlaylists": "清理合集和播放列表", "TaskCleanCollectionsAndPlaylistsDescription": "清理合集和播放列表中已不存在的项目。", "TaskAudioNormalization": "音频标准化", "TaskAudioNormalizationDescription": "扫描文件以寻找音频标准化数据。", "TaskDownloadMissingLyrics": "下载缺失的歌词", "TaskDownloadMissingLyricsDescription": "下载歌曲歌词", - "TaskMoveTrickplayImages": "迁移时间轴缩略图的存储位置", + "TaskMoveTrickplayImages": "迁移特技播放预览图的存储位置", "TaskExtractMediaSegments": "媒体片段扫描", "TaskExtractMediaSegmentsDescription": "从支持 MediaSegment 的插件中提取或获取媒体片段。", "TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的特技播放文件。" From b162e9290ba7e4265010079708e158399356635c Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Sun, 22 Sep 2024 22:58:15 +0800 Subject: [PATCH 104/159] Fix the diff between requested and actual resolution in RKMPP (#12680) --- .../MediaEncoding/EncodingHelper.cs | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 788bd03351..5322b2635d 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -5574,9 +5574,8 @@ namespace MediaBrowser.Controller.MediaEncoding var isFullAfbcPipeline = isEncoderSupportAfbc && isDrmInDrmOut && !doOclTonemap; var swapOutputWandH = doRkVppTranspose && swapWAndH; var outFormat = doOclTonemap ? "p010" : "nv12"; - var hwScalePrefix = doRkVppTranspose ? "vpp" : "scale"; - var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); - var doScaling = GetHwScaleFilter(hwScalePrefix, "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var hwScaleFilter = GetHwScaleFilter("vpp", "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var doScaling = GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); if (!hasSubs || doRkVppTranspose @@ -5588,7 +5587,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(doScaling) && !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f)) { - var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/8:h=ih/8:format={outFormat}:afbc=1"; + var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={outFormat}:afbc=1"; mainFilters.Add(hwScaleFilterFirstPass); } @@ -5980,19 +5979,6 @@ namespace MediaBrowser.Controller.MediaEncoding } } - var whichCodec = videoStream.Codec; - if (string.Equals(whichCodec, "avc", StringComparison.OrdinalIgnoreCase)) - { - whichCodec = "h264"; - } - else if (string.Equals(whichCodec, "h265", StringComparison.OrdinalIgnoreCase)) - { - whichCodec = "hevc"; - } - - // Avoid a second attempt if no hardware acceleration is being used - options.HardwareDecodingCodecs = options.HardwareDecodingCodecs.Where(c => !string.Equals(c, whichCodec, StringComparison.OrdinalIgnoreCase)).ToArray(); - // leave blank so ffmpeg will decide return null; } From bcc818f397789be7d487e58726a6dd415546f194 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sun, 22 Sep 2024 16:58:23 +0200 Subject: [PATCH 105/159] Fix DeviceProfile.Id should be nullable (#12679) --- MediaBrowser.Model/Dlna/DeviceProfile.cs | 2 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs index f689576222..995b7633a9 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfile.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -22,7 +22,7 @@ public class DeviceProfile /// /// Gets or sets the unique internal identifier. /// - public Guid Id { get; set; } + public Guid? Id { get; set; } /// /// Gets or sets the maximum allowed bitrate for all streamed content. diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 6c45f19468..4a666ac47b 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -65,7 +65,7 @@ namespace MediaBrowser.Model.Dlna if (streamInfo is not null) { streamInfo.DeviceId = options.DeviceId; - streamInfo.DeviceProfileId = options.Profile.Id.ToString("N", CultureInfo.InvariantCulture); + streamInfo.DeviceProfileId = options.Profile.Id?.ToString("N", CultureInfo.InvariantCulture); streams.Add(streamInfo); } } @@ -240,7 +240,7 @@ namespace MediaBrowser.Model.Dlna foreach (var stream in streams) { stream.DeviceId = options.DeviceId; - stream.DeviceProfileId = options.Profile.Id.ToString("N", CultureInfo.InvariantCulture); + stream.DeviceProfileId = options.Profile.Id?.ToString("N", CultureInfo.InvariantCulture); } return GetOptimalStream(streams, options.GetMaxBitrate(false) ?? 0); From 39747ee80b2f3f59274f54eeb60b56139936a942 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sun, 22 Sep 2024 18:25:25 +0200 Subject: [PATCH 106/159] Fix media segment operation name (#12682) --- Jellyfin.Api/Controllers/MediaSegmentsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs index e97704d48d..3dc5167a2e 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentsController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs @@ -45,7 +45,7 @@ public class MediaSegmentsController : BaseJellyfinApiController [HttpGet("{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetSegmentsAsync( + public async Task>> GetItemSegments( [FromRoute, Required] Guid itemId, [FromQuery] IEnumerable? includeSegmentTypes = null) { From 4dcae54035dbdcb621793bf71b6161c934d97d59 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sun, 22 Sep 2024 18:25:43 +0200 Subject: [PATCH 107/159] Fix GetTrickplayTileImage operation name (#12681) --- Jellyfin.Api/Controllers/TrickplayController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs index c1ff0f3401..2cf66144ce 100644 --- a/Jellyfin.Api/Controllers/TrickplayController.cs +++ b/Jellyfin.Api/Controllers/TrickplayController.cs @@ -80,7 +80,7 @@ public class TrickplayController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] - public async Task GetTrickplayTileImageAsync( + public async Task GetTrickplayTileImage( [FromRoute, Required] Guid itemId, [FromRoute, Required] int width, [FromRoute, Required] int index, From e21592f473845a587312dc08935f623d4d8f745b Mon Sep 17 00:00:00 2001 From: hoanghuy309 Date: Sun, 22 Sep 2024 18:06:29 +0000 Subject: [PATCH 108/159] Translated using Weblate (Vietnamese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/vi/ --- Emby.Server.Implementations/Localization/Core/vi.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json index 32e2f4bab1..f890ea74dc 100644 --- a/Emby.Server.Implementations/Localization/Core/vi.json +++ b/Emby.Server.Implementations/Localization/Core/vi.json @@ -131,5 +131,9 @@ "TaskAudioNormalization": "Chuẩn Hóa Âm Thanh", "TaskAudioNormalizationDescription": "Quét tập tin để tìm dữ liệu chuẩn hóa âm thanh.", "TaskDownloadMissingLyricsDescription": "Tải xuống lời cho bài hát", - "TaskDownloadMissingLyrics": "Tải xuống lời bị thiếu" + "TaskDownloadMissingLyrics": "Tải xuống lời bị thiếu", + "TaskExtractMediaSegmentsDescription": "Trích xuất hoặc lấy các phân đoạn phương tiện từ các plugin hỗ trợ MediaSegment.", + "TaskMoveTrickplayImages": "Di chuyển vị trí hình ảnh Trickplay", + "TaskMoveTrickplayImagesDescription": "Di chuyển các tập tin trickplay hiện có theo cài đặt thư viện.", + "TaskExtractMediaSegments": "Quét Phân Đoạn Phương Tiện" } From c108e5c4854e0b0a83361b05c39827186c545eb4 Mon Sep 17 00:00:00 2001 From: Nyanmisaka <799610810@qq.com> Date: Sun, 22 Sep 2024 19:49:50 +0000 Subject: [PATCH 109/159] Translated using Weblate (Chinese (Simplified Han script)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/ --- Emby.Server.Implementations/Localization/Core/zh-CN.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index a406a73b78..3256569e01 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -125,16 +125,16 @@ "TaskKeyframeExtractor": "关键帧提取器", "External": "外部", "HearingImpaired": "听力障碍", - "TaskRefreshTrickplayImages": "生成特技播放预览图", - "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成特技播放预览图。", + "TaskRefreshTrickplayImages": "生成进度条预览图", + "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成进度条预览图。", "TaskCleanCollectionsAndPlaylists": "清理合集和播放列表", "TaskCleanCollectionsAndPlaylistsDescription": "清理合集和播放列表中已不存在的项目。", "TaskAudioNormalization": "音频标准化", "TaskAudioNormalizationDescription": "扫描文件以寻找音频标准化数据。", "TaskDownloadMissingLyrics": "下载缺失的歌词", "TaskDownloadMissingLyricsDescription": "下载歌曲歌词", - "TaskMoveTrickplayImages": "迁移特技播放预览图的存储位置", + "TaskMoveTrickplayImages": "迁移进度条预览图的存储位置", "TaskExtractMediaSegments": "媒体片段扫描", "TaskExtractMediaSegmentsDescription": "从支持 MediaSegment 的插件中提取或获取媒体片段。", - "TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的特技播放文件。" + "TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的进度条预览图文件。" } From 5d5afe10e8094f14e5443e1fd3295bdf325f1566 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Mon, 23 Sep 2024 08:50:18 +0800 Subject: [PATCH 110/159] Fix incorrect input range for certain hw JPEG encoders (#12683) --- .../MediaEncoding/EncodingHelper.cs | 81 ++++++++++++++----- 1 file changed, 62 insertions(+), 19 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 5322b2635d..e9e8ed6182 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -71,6 +71,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegAdvancedTonemapMode = new Version(7, 0, 1); private readonly Version _minFFmpegAlteredVaVkInterop = new Version(7, 0, 1); private readonly Version _minFFmpegQsvVppTonemapOption = new Version(7, 0, 1); + private readonly Version _minFFmpegQsvVppOutRangeOption = new Version(7, 0, 1); private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled); @@ -3297,7 +3298,7 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Empty; } - public string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat) + private string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat, bool forceFullRange) { if (string.IsNullOrEmpty(hwTonemapSuffix)) { @@ -3307,7 +3308,7 @@ namespace MediaBrowser.Controller.MediaEncoding var args = string.Empty; var algorithm = options.TonemappingAlgorithm.ToString().ToLowerInvariant(); var mode = options.TonemappingMode.ToString().ToLowerInvariant(); - var range = options.TonemappingRange; + var range = forceFullRange ? TonemappingRange.pc : options.TonemappingRange; var rangeString = range.ToString().ToLowerInvariant(); if (string.Equals(hwTonemapSuffix, "vaapi", StringComparison.OrdinalIgnoreCase)) @@ -3377,7 +3378,7 @@ namespace MediaBrowser.Controller.MediaEncoding rangeString); } - public string GetLibplaceboFilter( + private string GetLibplaceboFilter( EncodingOptions options, string videoFormat, bool doTonemap, @@ -3386,7 +3387,8 @@ namespace MediaBrowser.Controller.MediaEncoding int? requestedWidth, int? requestedHeight, int? requestedMaxWidth, - int? requestedMaxHeight) + int? requestedMaxHeight, + bool forceFullRange) { var (outWidth, outHeight) = GetFixedOutputSize( videoWidth, @@ -3411,7 +3413,7 @@ namespace MediaBrowser.Controller.MediaEncoding var algorithm = options.TonemappingAlgorithm; var algorithmString = "clip"; var mode = options.TonemappingMode; - var range = options.TonemappingRange; + var range = forceFullRange ? TonemappingRange.pc : options.TonemappingRange; if (algorithm == TonemappingAlgorithm.bt2390) { @@ -3616,6 +3618,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isNvencEncoder = vidEncoder.Contains("nvenc", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isNvencEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isCuInCuOut = isNvDecoder && isNvencEncoder; var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.ReferenceFrameRate ?? 60) <= 30; @@ -3694,7 +3697,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw tonemap if (doCuTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "cuda", "yuv420p"); + var tonemapFilter = GetHwTonemapFilter(options, "cuda", "yuv420p", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -3823,6 +3826,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isAmfEncoder = vidEncoder.Contains("amf", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isAmfEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isDxInDxOut = isD3d11vaDecoder && isAmfEncoder; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); @@ -3902,7 +3906,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -4061,6 +4065,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isHwDecoder = isD3d11vaDecoder || isQsvDecoder; var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isQsvEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isQsvInQsvOut = isHwDecoder && isQsvEncoder; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); @@ -4119,6 +4124,8 @@ namespace MediaBrowser.Controller.MediaEncoding { var isRext = IsVideoStreamHevcRext(state); var twoPassVppTonemap = isRext; + var doVppFullRangeOut = isMjpegEncoder + && _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppOutRangeOption; var doVppProcamp = false; var procampParams = string.Empty; if (doVppTonemap) @@ -4145,7 +4152,7 @@ namespace MediaBrowser.Controller.MediaEncoding outFormat = twoPassVppTonemap ? "p010" : outFormat; var swapOutputWandH = doVppTranspose && swapWAndH; - var hwScalePrefix = (doVppTranspose || doVppTonemap) ? "vpp" : "scale"; + var hwScalePrefix = (doVppTranspose || doVppTonemap || doVppFullRangeOut) ? "vpp" : "scale"; var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, "qsv", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTranspose) @@ -4153,6 +4160,11 @@ namespace MediaBrowser.Controller.MediaEncoding hwScaleFilter += $":transpose={tranposeDir}"; } + if (!string.IsNullOrEmpty(hwScaleFilter) && doVppFullRangeOut) + { + hwScaleFilter += ":out_range=pc"; + } + if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTonemap) { hwScaleFilter += doVppProcamp ? procampParams : (twoPassVppTonemap ? string.Empty : ":tonemap=1"); @@ -4200,7 +4212,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -4317,6 +4329,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isHwDecoder = isVaapiDecoder || isQsvDecoder; var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isQsvEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isQsvInQsvOut = isHwDecoder && isQsvEncoder; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); @@ -4375,6 +4388,8 @@ namespace MediaBrowser.Controller.MediaEncoding { var hwFilterSuffix = isVaapiDecoder ? "vaapi" : "qsv"; var isRext = IsVideoStreamHevcRext(state); + var doVppFullRangeOut = isMjpegEncoder + && _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppOutRangeOption; // INPUT vaapi/qsv surface(vram) // hw deint @@ -4394,7 +4409,7 @@ namespace MediaBrowser.Controller.MediaEncoding outFormat = (doTonemap && isRext) ? "p010" : outFormat; var swapOutputWandH = isQsvDecoder && doVppTranspose && swapWAndH; - var hwScalePrefix = (isQsvDecoder && doVppTranspose) ? "vpp" : "scale"; + var hwScalePrefix = (isQsvDecoder && (doVppTranspose || doVppFullRangeOut)) ? "vpp" : "scale"; var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, hwFilterSuffix, outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); if (!string.IsNullOrEmpty(hwScaleFilter) && isQsvDecoder && doVppTranspose) @@ -4402,6 +4417,13 @@ namespace MediaBrowser.Controller.MediaEncoding hwScaleFilter += $":transpose={tranposeDir}"; } + if (!string.IsNullOrEmpty(hwScaleFilter) + && ((isVaapiDecoder && isMjpegEncoder) + || (isQsvDecoder && doVppFullRangeOut))) + { + hwScaleFilter += ":out_range=pc"; + } + // allocate extra pool sizes for vaapi vpp scale if (!string.IsNullOrEmpty(hwScaleFilter) && isVaapiDecoder) { @@ -4422,7 +4444,7 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add("format=vaapi"); } - var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); if (isQsvDecoder) @@ -4442,7 +4464,7 @@ namespace MediaBrowser.Controller.MediaEncoding // ocl tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -4633,6 +4655,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isVaapiEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isVaInVaOut = isVaapiDecoder && isVaapiEncoder; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); @@ -4708,6 +4731,11 @@ namespace MediaBrowser.Controller.MediaEncoding var outFormat = doTonemap ? (isRext ? "p010" : string.Empty) : "nv12"; var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", outFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder) + { + hwScaleFilter += ":out_range=pc"; + } + // allocate extra pool sizes for vaapi vpp if (!string.IsNullOrEmpty(hwScaleFilter)) { @@ -4721,7 +4749,7 @@ namespace MediaBrowser.Controller.MediaEncoding // vaapi vpp tonemap if (doVaVppTonemap && isVaapiDecoder) { - var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -4734,7 +4762,7 @@ namespace MediaBrowser.Controller.MediaEncoding // ocl tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -4858,6 +4886,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isVaapiEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); @@ -4953,6 +4982,12 @@ namespace MediaBrowser.Controller.MediaEncoding // hw scale var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", "nv12", false, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + + if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder) + { + hwScaleFilter += ":out_range=pc"; + } + mainFilters.Add(hwScaleFilter); } } @@ -4973,7 +5008,7 @@ namespace MediaBrowser.Controller.MediaEncoding // vk libplacebo if (doVkTonemap || hasSubs) { - var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH, isMjpegEncoder); mainFilters.Add(libplaceboFilter); mainFilters.Add("format=vulkan"); } @@ -5088,6 +5123,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isVaapiEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isVaInVaOut = isVaapiDecoder && isVaapiEncoder; var isi965Driver = _mediaEncoder.IsVaapiDeviceInteli965; var isAmdDriver = _mediaEncoder.IsVaapiDeviceAmd; @@ -5149,6 +5185,11 @@ namespace MediaBrowser.Controller.MediaEncoding outFormat = doOclTonemap ? string.Empty : "nv12"; var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", outFormat, false, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder) + { + hwScaleFilter += ":out_range=pc"; + } + // allocate extra pool sizes for vaapi vpp if (!string.IsNullOrEmpty(hwScaleFilter)) { @@ -5177,7 +5218,7 @@ namespace MediaBrowser.Controller.MediaEncoding // ocl tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -5303,6 +5344,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase); var isVtDecoder = vidDecoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase); + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var inW = state.VideoStream?.Width; var inH = state.VideoStream?.Height; @@ -5384,7 +5426,7 @@ namespace MediaBrowser.Controller.MediaEncoding // Metal tonemap if (doMetalTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "videotoolbox", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "videotoolbox", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -5507,6 +5549,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isRkmppEncoder = vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase); var isSwDecoder = !isRkmppDecoder; var isSwEncoder = !isRkmppEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isDrmInDrmOut = isRkmppDecoder && isRkmppEncoder; var isEncoderSupportAfbc = isRkmppEncoder && (vidEncoder.Contains("h264", StringComparison.OrdinalIgnoreCase) @@ -5573,7 +5616,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isFullAfbcPipeline = isEncoderSupportAfbc && isDrmInDrmOut && !doOclTonemap; var swapOutputWandH = doRkVppTranspose && swapWAndH; - var outFormat = doOclTonemap ? "p010" : "nv12"; + var outFormat = doOclTonemap ? "p010" : (isMjpegEncoder ? "bgra" : "nv12"); // RGA only support full range in rgb fmts var hwScaleFilter = GetHwScaleFilter("vpp", "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); var doScaling = GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); @@ -5616,7 +5659,7 @@ namespace MediaBrowser.Controller.MediaEncoding // ocl tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } From 9ec85a0f180dd3ea1582ad60f3fb8d55861f2fdf Mon Sep 17 00:00:00 2001 From: elfalem Date: Sun, 22 Sep 2024 20:50:29 -0400 Subject: [PATCH 111/159] Allow Playlists access for users with allowed tags configured (#12686) --- CONTRIBUTORS.md | 1 + MediaBrowser.Controller/Entities/BaseItem.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 5b94e04e15..a9deb1c4a2 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -191,6 +191,7 @@ - [pret0rian8](https://github.com/pret0rian) - [jaina heartles](https://github.com/heartles) - [oxixes](https://github.com/oxixes) + - [elfalem](https://github.com/elfalem) # Emby Contributors diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 414488853f..eb605f6c87 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1608,7 +1608,7 @@ namespace MediaBrowser.Controller.Entities } var parent = GetParents().FirstOrDefault() ?? this; - if (parent is UserRootFolder or AggregateFolder) + if (parent is UserRootFolder or AggregateFolder or UserView) { return true; } From aed00733f88fb9a50fc51816fd05a4438c9d5c42 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Sep 2024 19:44:37 -0600 Subject: [PATCH 112/159] Update dependency xunit to 2.9.1 (#12687) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Cody Robibero --- Directory.Packages.props | 2 +- tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs | 8 ++++---- .../Parsers/EpisodeNfoProviderTests.cs | 2 +- .../Parsers/MovieNfoParserTests.cs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2c16bc9d15..b16b7d78c0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -86,6 +86,6 @@ - + \ No newline at end of file diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs index 3005a4416c..6b13986957 100644 --- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs @@ -25,8 +25,8 @@ namespace Jellyfin.Naming.Tests.Video files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); - Assert.Single(result.Where(v => v.ExtraType is null)); - Assert.Single(result.Where(v => v.ExtraType is not null)); + Assert.Single(result, v => v.ExtraType is null); + Assert.Single(result, v => v.ExtraType is not null); } [Fact] @@ -44,8 +44,8 @@ namespace Jellyfin.Naming.Tests.Video files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); - Assert.Single(result.Where(v => v.ExtraType is null)); - Assert.Single(result.Where(v => v.ExtraType is not null)); + Assert.Single(result, v => v.ExtraType is null); + Assert.Single(result, v => v.ExtraType is not null); Assert.Equal(2, result[0].AlternateVersions.Count); } diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs index 3721d1f7ac..12d6e1934d 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs @@ -157,7 +157,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers _parser.Fetch(result, "Test Data/Sonarr-Thumb.nfo", CancellationToken.None); - Assert.Single(result.RemoteImages.Where(x => x.Type == ImageType.Primary)); + Assert.Single(result.RemoteImages, x => x.Type == ImageType.Primary); Assert.Equal("https://artworks.thetvdb.com/banners/episodes/359095/7081317.jpg", result.RemoteImages.First(x => x.Type == ImageType.Primary).Url); } diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs index 5bc4abd06d..075c70da88 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs @@ -220,7 +220,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers _parser.Fetch(result, "Test Data/Fanart.nfo", CancellationToken.None); - Assert.Single(result.RemoteImages.Where(x => x.Type == ImageType.Backdrop)); + Assert.Single(result.RemoteImages, x => x.Type == ImageType.Backdrop); Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a5332c7b5e77.jpg", result.RemoteImages.First(x => x.Type == ImageType.Backdrop).Url); } From 8a456bf8957923b934873bab89e3984512a5425b Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com> Date: Mon, 23 Sep 2024 18:01:45 +0300 Subject: [PATCH 113/159] Escape quotes in the subtitle path (#12690) --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 517d135d3b..7ae1fbbb10 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -1104,7 +1104,11 @@ namespace MediaBrowser.MediaEncoding.Encoder // https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping // We need to double escape - return path.Replace('\\', '/').Replace(":", "\\:", StringComparison.Ordinal).Replace("'", @"'\\\''", StringComparison.Ordinal); + return path + .Replace('\\', '/') + .Replace(":", "\\:", StringComparison.Ordinal) + .Replace("'", @"'\\\''", StringComparison.Ordinal) + .Replace("\"", "\\\"", StringComparison.Ordinal); } /// From a0204ada2f97b73f932e1ec40befd6cfa75e8e16 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Mon, 23 Sep 2024 23:02:31 +0800 Subject: [PATCH 114/159] Fix intel Xe kernel driver cannot be used with QSV (#12691) --- .../MediaEncoding/EncodingHelper.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index e9e8ed6182..b05d2ffb7b 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -72,6 +72,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegAlteredVaVkInterop = new Version(7, 0, 1); private readonly Version _minFFmpegQsvVppTonemapOption = new Version(7, 0, 1); private readonly Version _minFFmpegQsvVppOutRangeOption = new Version(7, 0, 1); + private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1); private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled); @@ -872,13 +873,15 @@ namespace MediaBrowser.Controller.MediaEncoding options); } - private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string srcDeviceAlias, string alias) + private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string vendorId, string srcDeviceAlias, string alias) { alias ??= VaapiAlias; + var haveVendorId = !string.IsNullOrEmpty(vendorId) + && _mediaEncoder.EncoderVersion >= _minFFmpegVaapiDeviceVendorId; - // 'renderNodePath' has higher priority than 'kernelDriver' + // Priority: 'renderNodePath' > 'vendorId' > 'kernelDriver' var driverOpts = string.IsNullOrEmpty(renderNodePath) - ? (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver) + ? (haveVendorId ? $",vendor_id={vendorId}" : (string.IsNullOrEmpty(kernelDriver) ? string.Empty : $",kernel_driver={kernelDriver}")) : renderNodePath; // 'driver' behaves similarly to env LIBVA_DRIVER_NAME @@ -913,7 +916,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (OperatingSystem.IsLinux()) { // derive qsv from vaapi device - return GetVaapiDeviceArgs(renderNodePath, "iHD", "i915", null, VaapiAlias) + arg + "@" + VaapiAlias; + return GetVaapiDeviceArgs(renderNodePath, "iHD", "i915", "0x8086", null, VaapiAlias) + arg + "@" + VaapiAlias; } if (OperatingSystem.IsWindows()) @@ -1008,14 +1011,14 @@ namespace MediaBrowser.Controller.MediaEncoding if (_mediaEncoder.IsVaapiDeviceInteliHD) { - args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "iHD", null, null, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "iHD", null, null, null, VaapiAlias)); } else if (_mediaEncoder.IsVaapiDeviceInteli965) { // Only override i965 since it has lower priority than iHD in libva lookup. Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME", "i965"); Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME_JELLYFIN", "i965"); - args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "i965", null, null, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "i965", null, null, null, VaapiAlias)); } var filterDevArgs = string.Empty; @@ -1039,7 +1042,7 @@ namespace MediaBrowser.Controller.MediaEncoding && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier) { args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias)); - args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(null, null, null, null, DrmAlias, VaapiAlias)); args.Append(GetVulkanDeviceArgs(0, null, DrmAlias, VulkanAlias)); // libplacebo wants an explicitly set vulkan filter device. @@ -1047,7 +1050,7 @@ namespace MediaBrowser.Controller.MediaEncoding } else { - args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, null, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, null, null, VaapiAlias)); filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias); if (doOclTonemap) From 0539fdc5e3a164b694d12d8d3f8437e2cc5b6457 Mon Sep 17 00:00:00 2001 From: gnattu Date: Mon, 23 Sep 2024 23:09:08 +0800 Subject: [PATCH 115/159] Fix libx264/libx265 auto preset (#12692) --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index b05d2ffb7b..557eb5e329 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1444,7 +1444,13 @@ namespace MediaBrowser.Controller.MediaEncoding var encoderPreset = preset ?? defaultPreset; if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) || isLibX265) { - param += " -preset " + encoderPreset.ToString().ToLowerInvariant(); + var presetString = encoderPreset switch + { + EncoderPreset.auto => EncoderPreset.veryfast.ToString().ToLowerInvariant(), + _ => encoderPreset.ToString().ToLowerInvariant() + }; + + param += " -preset " + presetString; int encodeCrf = encodingOptions.H264Crf; if (isLibX265) From 3c639c2e80f2a17eea3f5f1a70c1b287bc99aba4 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Mon, 23 Sep 2024 09:09:23 -0600 Subject: [PATCH 116/159] Tweak Trickplay migration for speed (#12643) --- .../Trickplay/TrickplayManager.cs | 8 ++- .../Migrations/Routines/MoveTrickplayFiles.cs | 59 ++++++++++++++----- .../Trickplay/ITrickplayManager.cs | 4 +- .../Trickplay/TrickplayMoveImagesTask.cs | 51 +++++++++------- 4 files changed, 83 insertions(+), 39 deletions(-) diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index 861037c1fe..73e31279f4 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -455,16 +455,18 @@ public class TrickplayManager : ITrickplayManager } /// - public async Task> GetTrickplayItemsAsync() + public async Task> GetTrickplayItemsAsync(int limit, int offset) { - List trickplayItems; + IReadOnlyList trickplayItems; var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { trickplayItems = await dbContext.TrickplayInfos .AsNoTracking() - .Select(i => i.ItemId) + .OrderBy(i => i.ItemId) + .Skip(offset) + .Take(limit) .ToListAsync() .ConfigureAwait(false); } diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs index 301541b6ce..c1a9e88949 100644 --- a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs +++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs @@ -1,10 +1,15 @@ using System; +using System.Diagnostics; using System.Globalization; using System.IO; +using System.Linq; +using Jellyfin.Data.Enums; +using MediaBrowser.Common; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.Routines; @@ -16,6 +21,7 @@ public class MoveTrickplayFiles : IMigrationRoutine private readonly ITrickplayManager _trickplayManager; private readonly IFileSystem _fileSystem; private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. @@ -23,11 +29,13 @@ public class MoveTrickplayFiles : IMigrationRoutine /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - public MoveTrickplayFiles(ITrickplayManager trickplayManager, IFileSystem fileSystem, ILibraryManager libraryManager) + /// The logger. + public MoveTrickplayFiles(ITrickplayManager trickplayManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILogger logger) { _trickplayManager = trickplayManager; _fileSystem = fileSystem; _libraryManager = libraryManager; + _logger = logger; } /// @@ -42,26 +50,49 @@ public class MoveTrickplayFiles : IMigrationRoutine /// public void Perform() { - var trickplayItems = _trickplayManager.GetTrickplayItemsAsync().GetAwaiter().GetResult(); - foreach (var itemId in trickplayItems) - { - var resolutions = _trickplayManager.GetTrickplayResolutions(itemId).GetAwaiter().GetResult(); - var item = _libraryManager.GetItemById(itemId); - if (item is null) - { - continue; - } + const int Limit = 100; + int itemCount = 0, offset = 0, previousCount; - foreach (var resolution in resolutions) + var sw = Stopwatch.StartNew(); + var trickplayQuery = new InternalItemsQuery + { + MediaTypes = [MediaType.Video], + SourceTypes = [SourceType.Library], + IsVirtualItem = false, + IsFolder = false + }; + + do + { + var trickplayInfos = _trickplayManager.GetTrickplayItemsAsync(Limit, offset).GetAwaiter().GetResult(); + previousCount = trickplayInfos.Count; + offset += Limit; + + trickplayQuery.ItemIds = trickplayInfos.Select(i => i.ItemId).Distinct().ToArray(); + var items = _libraryManager.GetItemList(trickplayQuery); + foreach (var trickplayInfo in trickplayInfos) { - var oldPath = GetOldTrickplayDirectory(item, resolution.Key); - var newPath = _trickplayManager.GetTrickplayDirectory(item, resolution.Value.TileWidth, resolution.Value.TileHeight, resolution.Value.Width, false); + var item = items.OfType