From 85965741f57e709133fbaf5ba59ed45bbd3b5d26 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Sun, 8 Nov 2020 01:39:32 +0800 Subject: add initial support for HEVC over FMP4-HLS --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 76 +++++++++++++++++++++--- 1 file changed, 68 insertions(+), 8 deletions(-) (limited to 'Jellyfin.Api/Controllers/DynamicHlsController.cs') diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index e07690e11..1247ef502 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1136,11 +1136,19 @@ namespace Jellyfin.Api.Controllers var segmentLengths = GetSegmentLengths(state); + var segmentContainer = state.Request.SegmentContainer ?? "ts"; + + // http://ffmpeg.org/ffmpeg-all.html#toc-hls-2 + var isHlsInFmp4 = string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase); + var hlsVersion = isHlsInFmp4 ? "7" : "3"; + var builder = new StringBuilder(); builder.AppendLine("#EXTM3U") .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD") - .AppendLine("#EXT-X-VERSION:3") + .Append("#EXT-X-VERSION:") + .Append(hlsVersion) + .AppendLine() .Append("#EXT-X-TARGETDURATION:") .Append(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength)) .AppendLine() @@ -1150,6 +1158,18 @@ namespace Jellyfin.Api.Controllers var segmentExtension = GetSegmentFileExtension(streamingRequest.SegmentContainer); var queryString = Request.QueryString; + if (isHlsInFmp4) + { + builder.Append("#EXT-X-MAP:URI=\"") + .Append("hls1/") + .Append(name) + .Append("/-1") + .Append(segmentExtension) + .Append(queryString) + .Append('"') + .AppendLine(); + } + foreach (var length in segmentLengths) { builder.Append("#EXTINF:") @@ -1232,7 +1252,13 @@ namespace Jellyfin.Api.Controllers var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength; - if (currentTranscodingIndex == null) + if (segmentId == -1) + { + _logger.LogDebug("Starting transcoding because fmp4 header file is being requested"); + startTranscoding = true; + segmentId = 0; + } + else if (currentTranscodingIndex == null) { _logger.LogDebug("Starting transcoding because currentTranscodingIndex=null"); startTranscoding = true; @@ -1347,13 +1373,24 @@ namespace Jellyfin.Api.Controllers var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty; - var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request.SegmentContainer); + var outputPrefix = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)); + var outputExtension = GetSegmentFileExtension(state.Request.SegmentContainer); + var outputTsArg = outputPrefix + "%d" + outputExtension; var segmentFormat = GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.'); if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase)) { segmentFormat = "mpegts"; } + else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)) + { + var outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\""; + segmentFormat = "fmp4" + outputFmp4HeaderArg; + } + else + { + _logger.LogError("Invalid HLS segment container: " + segmentFormat); + } var maxMuxingQueueSize = encodingOptions.MaxMuxingQueueSize > 128 ? encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture) @@ -1384,7 +1421,7 @@ namespace Jellyfin.Api.Controllers { if (EncodingHelper.IsCopyCodec(audioCodec)) { - return "-acodec copy"; + return "-acodec copy -strict -2"; } var audioTranscodeParams = new List(); @@ -1416,10 +1453,10 @@ namespace Jellyfin.Api.Controllers if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec)) { - return "-codec:a:0 copy -copypriorss:a:0 0"; + return "-codec:a:0 copy -strict -2 -copypriorss:a:0 0"; } - return "-codec:a:0 copy"; + return "-codec:a:0 copy -strict -2"; } var args = "-codec:a:0 " + audioCodec; @@ -1459,6 +1496,15 @@ namespace Jellyfin.Api.Controllers var args = "-codec:v:0 " + codec; + // Prefer hvc1 to hev1 + 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)) + { + args += " -tag:v:0 hvc1"; + } + // if (state.EnableMpegtsM2TsMode) // { // args += " -mpegts_m2ts_mode 1"; @@ -1505,18 +1551,32 @@ namespace Jellyfin.Api.Controllers args += " " + _encodingHelper.GetVideoQualityParam(state, codec, encodingOptions, "veryfast"); - // Unable to force key frames using these hw encoders, set key frames by GOP + // Unable to force key frames using these encoders, set key frames by GOP if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase)) + || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc_amf", StringComparison.OrdinalIgnoreCase)) { args += " " + gopArg; } + else if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)) + { + args += " " + keyFrameArg; + } else { args += " " + keyFrameArg + gopArg; } + // Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now + if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)) + { + args += " -bf 0"; + } + // args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0"; var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; -- cgit v1.2.3 From 1abd3d1bd8049e0fad557b95ee7d76916883b539 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Sun, 8 Nov 2020 07:13:00 +0000 Subject: fix the fmp4 header file generate on linux --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) (limited to 'Jellyfin.Api/Controllers/DynamicHlsController.cs') diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 1247ef502..9941c9f6e 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -1373,7 +1374,8 @@ namespace Jellyfin.Api.Controllers var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty; - var outputPrefix = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)); + var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath); + var outputPrefix = Path.Combine(Path.GetDirectoryName(outputPath), outputFileNameWithoutExtension); var outputExtension = GetSegmentFileExtension(state.Request.SegmentContainer); var outputTsArg = outputPrefix + "%d" + outputExtension; @@ -1384,7 +1386,19 @@ namespace Jellyfin.Api.Controllers } else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)) { - var outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\""; + var outputFmp4HeaderArg = string.Empty; + var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + if (isWindows) + { + // on Windows, the path of fmp4 header file needs to be configured + outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\""; + } + else + { + // on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder + outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\""; + } + segmentFormat = "fmp4" + outputFmp4HeaderArg; } else -- cgit v1.2.3 From 5048719a64ce914f859e058c1a7b03258255836e Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Sun, 8 Nov 2020 09:01:58 +0000 Subject: minor changes per suggestions --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 6 +- Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs | 99 ++++++++++++++-------- .../MediaEncoding/EncodingHelper.cs | 8 +- 3 files changed, 72 insertions(+), 41 deletions(-) (limited to 'Jellyfin.Api/Controllers/DynamicHlsController.cs') diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 9941c9f6e..6b1618421 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1412,7 +1412,7 @@ namespace Jellyfin.Api.Controllers return string.Format( CultureInfo.InvariantCulture, - "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -individual_header_trailer 0 -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"", + "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts 0 -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -individual_header_trailer 0 -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"", inputModifier, _encodingHelper.GetInputArgument(state, encodingOptions), threads, @@ -1576,7 +1576,9 @@ namespace Jellyfin.Api.Controllers args += " " + gopArg; } else if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)) + || string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) { args += " " + keyFrameArg; } diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs index 3d7a9d9a0..4999fcc62 100644 --- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs @@ -9,13 +9,38 @@ namespace Jellyfin.Api.Helpers /// public static class HlsCodecStringHelpers { + /// + /// Codec name for MP3. + /// + public const string MP3 = "mp4a.40.34"; + + /// + /// Codec name for AC-3. + /// + public const string AC3 = "mp4a.a5"; + + /// + /// Codec name for E-AC-3. + /// + public const string EAC3 = "mp4a.a6"; + + /// + /// Codec name for FLAC. + /// + public const string FLAC = "fLaC"; + + /// + /// Codec name for ALAC. + /// + public const string ALAC = "alac"; + /// /// Gets a MP3 codec string. /// /// MP3 codec string. public static string GetMP3String() { - return "mp4a.40.34"; + return MP3; } /// @@ -40,6 +65,42 @@ namespace Jellyfin.Api.Helpers return result.ToString(); } + /// + /// Gets an AC-3 codec string. + /// + /// AC-3 codec string. + public static string GetAC3String() + { + return AC3; + } + + /// + /// Gets an E-AC-3 codec string. + /// + /// E-AC-3 codec string. + public static string GetEAC3String() + { + return EAC3; + } + + /// + /// Gets an FLAC codec string. + /// + /// FLAC codec string. + public static string GetFLACString() + { + return FLAC; + } + + /// + /// Gets an ALAC codec string. + /// + /// ALAC codec string. + public static string GetALACString() + { + return ALAC; + } + /// /// Gets a H.264 codec string. /// @@ -104,41 +165,5 @@ namespace Jellyfin.Api.Helpers return result.ToString(); } - - /// - /// Gets an AC-3 codec string. - /// - /// AC-3 codec string. - public static string GetAC3String() - { - return "mp4a.a5"; - } - - /// - /// Gets an E-AC-3 codec string. - /// - /// E-AC-3 codec string. - public static string GetEAC3String() - { - return "mp4a.a6"; - } - - /// - /// Gets an FLAC codec string. - /// - /// FLAC codec string. - public static string GetFLACString() - { - return "fLaC"; - } - - /// - /// Gets an ALAC codec string. - /// - /// ALAC codec string. - public static string GetALACString() - { - return "alac"; - } } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 87670e2eb..6e348f1e6 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -662,6 +662,10 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)) { + // Transcode to level 5.0 and lower for maximum compatibility + // level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it + // https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels + // MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880, if (requestLevel >= 150) { return "150"; @@ -3293,7 +3297,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.RunTimeTicks.HasValue && state.BaseRequest.CopyTimestamps) { - args += " -copyts -avoid_negative_ts disabled -start_at_zero"; + args += " -copyts -avoid_negative_ts 0 -start_at_zero"; } if (!state.RunTimeTicks.HasValue) @@ -3341,7 +3345,7 @@ namespace MediaBrowser.Controller.MediaEncoding args += " -copyts"; } - args += " -avoid_negative_ts disabled"; + args += " -avoid_negative_ts 0"; if (!(state.SubtitleStream != null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)) { -- cgit v1.2.3 From 0b01acbe9172872f2685b115c219efd98619b4bc Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Wed, 11 Nov 2020 02:03:53 +0000 Subject: Apply suggestions from code review Co-authored-by: BaronGreenback --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 4 +-- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 17 ++++++------ .../MediaEncoding/EncodingHelper.cs | 30 +++++++++++----------- 3 files changed, 26 insertions(+), 25 deletions(-) (limited to 'Jellyfin.Api/Controllers/DynamicHlsController.cs') diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 6b1618421..4cf4c24d7 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1565,7 +1565,7 @@ namespace Jellyfin.Api.Controllers args += " " + _encodingHelper.GetVideoQualityParam(state, codec, encodingOptions, "veryfast"); - // Unable to force key frames using these encoders, set key frames by GOP + // Unable to force key frames using these encoders, set key frames by GOP. if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase) @@ -1587,7 +1587,7 @@ namespace Jellyfin.Api.Controllers args += " " + keyFrameArg + gopArg; } - // Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now + // Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now. if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)) { args += " -bf 0"; diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 4999a582e..d6fa6e98d 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -206,7 +206,7 @@ namespace Jellyfin.Api.Helpers if (state.VideoStream != null && state.VideoRequest != null) { - // Provide SDR HEVC entrance for backward compatibility + // Provide SDR HEVC entrance for backward compatibility. if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && !string.IsNullOrEmpty(state.VideoStream.VideoRange) && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) @@ -215,7 +215,7 @@ namespace Jellyfin.Api.Helpers var requestedVideoProfiles = state.GetRequestedProfiles("hevc"); if (requestedVideoProfiles != null && requestedVideoProfiles.Length > 0) { - // Force HEVC Main Profile and disable video stream copy + // Force HEVC Main Profile and disable video stream copy. state.OutputVideoCodec = "hevc"; var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(",", requestedVideoProfiles), "main"); sdrVideoUrl += "&AllowVideoStreamCopy=false"; @@ -232,7 +232,7 @@ namespace Jellyfin.Api.Helpers } } - // Provide Level 5.0 entrance for backward compatibility + // Provide Level 5.0 entrance for backward compatibility. // e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video, // but in fact it is capable of playing videos up to Level 6.1. if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) @@ -245,13 +245,13 @@ namespace Jellyfin.Api.Helpers var playlistCodecsField = new StringBuilder(); AppendPlaylistCodecsField(playlistCodecsField, state); - // Force the video level to 5.0 + // Force the video level to 5.0. var originalLevel = state.VideoStream.Level; state.VideoStream.Level = 150; var newPlaylistCodecsField = new StringBuilder(); AppendPlaylistCodecsField(newPlaylistCodecsField, state); - // Restore the video level + // Restore the video level. state.VideoStream.Level = originalLevel; var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField); builder.Append(newPlaylist); @@ -333,7 +333,7 @@ namespace Jellyfin.Api.Helpers } else { - // Currently we only encode to SDR + // Currently we only encode to SDR. builder.Append(",VIDEO-RANGE=SDR"); } } @@ -693,9 +693,10 @@ namespace Jellyfin.Api.Helpers private string ReplaceProfile(string url, string codec, string oldValue, string newValue) { + string profileStr = codec + "-profile="; return url.Replace( - codec + "-profile=" + oldValue, - codec + "-profile=" + newValue, + profileStr + oldValue, + profileStr + newValue, StringComparison.OrdinalIgnoreCase); } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 6e348f1e6..6255e1b61 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -662,10 +662,10 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)) { - // Transcode to level 5.0 and lower for maximum compatibility - // level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it + // Transcode to level 5.0 and lower for maximum compatibility. + // Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it. // https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels - // MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880, + // MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880. if (requestLevel >= 150) { return "150"; @@ -673,7 +673,7 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase)) { - // Clients may direct play higher than level 41, but there's no reason to transcode higher + // Clients may direct play higher than level 41, but there's no reason to transcode higher. if (requestLevel >= 41) { return "41"; @@ -843,7 +843,7 @@ namespace MediaBrowser.Controller.MediaEncoding else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc) || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc) { - // following preset will be deprecated in ffmpeg 4.4, use p1~p7 instead + // following preset will be deprecated in ffmpeg 4.4, use p1~p7 instead. switch (encodingOptions.EncoderPreset) { case "veryslow": @@ -976,7 +976,7 @@ namespace MediaBrowser.Controller.MediaEncoding var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault(); profile = Regex.Replace(profile, @"\s+", String.Empty); - // only libx264 support encoding H264 High 10 Profile, otherwise force High Profile + // Only libx264 support encoding H264 High 10 Profile, otherwise force High Profile. if (!string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) && profile != null && profile.IndexOf("high 10", StringComparison.OrdinalIgnoreCase) != -1) @@ -985,7 +985,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // h264_vaapi does not support Baseline profile, force Constrained Baseline in this case, - // which is compatible (and ugly) + // which is compatible (and ugly). if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) && profile != null && profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1) @@ -993,7 +993,7 @@ namespace MediaBrowser.Controller.MediaEncoding profile = "constrained_baseline"; } - // libx264, h264_qsv and h264_nvenc does not support Constrained Baseline profile, force Baseline in this case + // libx264, h264_qsv and h264_nvenc does not support Constrained Baseline profile, force Baseline in this case. if ((string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)) @@ -1003,7 +1003,7 @@ namespace MediaBrowser.Controller.MediaEncoding profile = "baseline"; } - // Currently hevc_amf only support encoding HEVC Main Profile, otherwise force Main Profile + // Currently hevc_amf only support encoding HEVC Main Profile, otherwise force Main Profile. if (!string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) && profile != null && profile.IndexOf("main 10", StringComparison.OrdinalIgnoreCase) != -1) @@ -1027,7 +1027,7 @@ namespace MediaBrowser.Controller.MediaEncoding { level = NormalizeTranscodingLevel(state, level); - // libx264, QSV, AMF, VAAPI can adjust the given level to match the output + // libx264, QSV, AMF, VAAPI can adjust the given level to match the output. if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)) { @@ -1035,7 +1035,7 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) { - // hevc_qsv use -level 51 instead of -level 153 + // hevc_qsv use -level 51 instead of -level 153. if (double.TryParse(level, NumberStyles.Any, _usCulture, out double hevcLevel)) { param += " -level " + hevcLevel / 3; @@ -1066,10 +1066,10 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase)) { - // libx265 only accept level option in -x265-params - // level option may cause libx265 to fail - // libx265 cannot adjust the given level, just throw an error - // TODO: set fine tuned params + // libx265 only accept level option in -x265-params. + // level option may cause libx265 to fail. + // libx265 cannot adjust the given level, just throw an error. + // TODO: set fine tuned params. param += " -x265-params:0 no-info=1"; } -- cgit v1.2.3 From 11c74cb65cd936f0fece57357beb7a27741edc24 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Wed, 11 Nov 2020 19:04:58 +0800 Subject: fix for no audio stream video --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 5 +++++ MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 5 +++++ MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs | 10 ++++++++++ 3 files changed, 20 insertions(+) (limited to 'Jellyfin.Api/Controllers/DynamicHlsController.cs') diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 4cf4c24d7..0581928a0 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1429,6 +1429,11 @@ namespace Jellyfin.Api.Controllers private string GetAudioArguments(StreamState state, EncodingOptions encodingOptions) { + if (state.AudioStream == null) + { + return string.Empty; + } + var audioCodec = _encodingHelper.GetAudioEncoder(state); if (!state.IsOutputVideo) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index e8b4869ee..caf3ef169 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1518,6 +1518,11 @@ namespace MediaBrowser.Controller.MediaEncoding /// System.Nullable{System.Int32}. public int? GetNumAudioChannelsParam(EncodingJobInfo state, MediaStream audioStream, string outputAudioCodec) { + if (audioStream == null) + { + return null; + } + var request = state.BaseRequest; var inputChannels = audioStream?.Channels; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 6e9362cd1..09e7889b9 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -593,6 +593,11 @@ namespace MediaBrowser.Controller.MediaEncoding { get { + if (VideoStream == null) + { + return null; + } + if (EncodingHelper.IsCopyCodec(OutputVideoCodec)) { return VideoStream?.Codec; @@ -606,6 +611,11 @@ namespace MediaBrowser.Controller.MediaEncoding { get { + if (AudioStream == null) + { + return null; + } + if (EncodingHelper.IsCopyCodec(OutputAudioCodec)) { return AudioStream?.Codec; -- cgit v1.2.3 From 8c0778e82743d999367ad560aefee636db460afb Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Sat, 14 Nov 2020 03:47:54 +0800 Subject: switch ffmpeg to hls muxer for live streaming segment muxer cannot make fMP4 init file. '-strict -2' option doesn't work with segment muxer for flac remuxing. --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 85 +++++----- Jellyfin.Api/Controllers/VideoHlsController.cs | 195 +++++++++++++++++++---- Jellyfin.Api/Helpers/HlsHelpers.cs | 70 +++++++- 3 files changed, 272 insertions(+), 78 deletions(-) (limited to 'Jellyfin.Api/Controllers/DynamicHlsController.cs') diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 0581928a0..d4ae124b9 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -41,6 +41,9 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] public class DynamicHlsController : BaseJellyfinApiController { + private const string DefaultEncoderPreset = "veryfast"; + private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls; + private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; private readonly IDlnaManager _dlnaManager; @@ -56,8 +59,7 @@ namespace Jellyfin.Api.Controllers private readonly ILogger _logger; private readonly EncodingHelper _encodingHelper; private readonly DynamicHlsHelper _dynamicHlsHelper; - - private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls; + private readonly EncodingOptions _encodingOptions; /// /// Initializes a new instance of the class. @@ -92,6 +94,8 @@ namespace Jellyfin.Api.Controllers ILogger logger, DynamicHlsHelper dynamicHlsHelper) { + _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration); + _libraryManager = libraryManager; _userManager = userManager; _dlnaManager = dlnaManager; @@ -106,8 +110,7 @@ namespace Jellyfin.Api.Controllers _transcodingJobHelper = transcodingJobHelper; _logger = logger; _dynamicHlsHelper = dynamicHlsHelper; - - _encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration); + _encodingOptions = serverConfigurationManager.GetEncodingOptions(); } /// @@ -1255,7 +1258,7 @@ namespace Jellyfin.Api.Controllers if (segmentId == -1) { - _logger.LogDebug("Starting transcoding because fmp4 header file is being requested"); + _logger.LogDebug("Starting transcoding because fmp4 init file is being requested"); startTranscoding = true; segmentId = 0; } @@ -1291,11 +1294,10 @@ namespace Jellyfin.Api.Controllers streamingRequest.StartTimeTicks = GetStartPositionTicks(state, segmentId); state.WaitForPath = segmentPath; - var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); job = await _transcodingJobHelper.StartFfMpeg( state, playlistPath, - GetCommandLineArguments(playlistPath, encodingOptions, state, true, segmentId), + GetCommandLineArguments(playlistPath, state, true, segmentId), Request, _transcodingJobType, cancellationTokenSource).ConfigureAwait(false); @@ -1351,11 +1353,10 @@ namespace Jellyfin.Api.Controllers return result.ToArray(); } - private string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding, int startNumber) + private string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding, int startNumber) { - var videoCodec = _encodingHelper.GetVideoEncoder(state, encodingOptions); - - var threads = _encodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec); + var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); + var threads = _encodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec); if (state.BaseRequest.BreakOnNonKeyFrames) { @@ -1367,11 +1368,9 @@ namespace Jellyfin.Api.Controllers state.BaseRequest.BreakOnNonKeyFrames = false; } - var inputModifier = _encodingHelper.GetInputModifier(state, encodingOptions); - // If isEncoding is true we're actually starting ffmpeg var startNumberParam = isEncoding ? startNumber.ToString(CultureInfo.InvariantCulture) : "0"; - + var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions); var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty; var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath); @@ -1379,7 +1378,7 @@ namespace Jellyfin.Api.Controllers var outputExtension = GetSegmentFileExtension(state.Request.SegmentContainer); var outputTsArg = outputPrefix + "%d" + outputExtension; - var segmentFormat = GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.'); + var segmentFormat = outputExtension.TrimStart('.'); if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase)) { segmentFormat = "mpegts"; @@ -1406,19 +1405,19 @@ namespace Jellyfin.Api.Controllers _logger.LogError("Invalid HLS segment container: " + segmentFormat); } - var maxMuxingQueueSize = encodingOptions.MaxMuxingQueueSize > 128 - ? encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture) + var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128 + ? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture) : "128"; return string.Format( CultureInfo.InvariantCulture, "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts 0 -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -individual_header_trailer 0 -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"", inputModifier, - _encodingHelper.GetInputArgument(state, encodingOptions), + _encodingHelper.GetInputArgument(state, _encodingOptions), threads, mapArgs, - GetVideoArguments(state, encodingOptions, startNumber), - GetAudioArguments(state, encodingOptions), + GetVideoArguments(state, startNumber), + GetAudioArguments(state), maxMuxingQueueSize, state.SegmentLength.ToString(CultureInfo.InvariantCulture), segmentFormat, @@ -1427,7 +1426,12 @@ namespace Jellyfin.Api.Controllers outputPath).Trim(); } - private string GetAudioArguments(StreamState state, EncodingOptions encodingOptions) + /// + /// Gets the audio arguments for transcoding. + /// + /// The . + /// The command line arguments for audio transcoding. + private string GetAudioArguments(StreamState state) { if (state.AudioStream == null) { @@ -1468,7 +1472,7 @@ namespace Jellyfin.Api.Controllers if (EncodingHelper.IsCopyCodec(audioCodec)) { - var videoCodec = _encodingHelper.GetVideoEncoder(state, encodingOptions); + var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec)) { @@ -1499,23 +1503,34 @@ namespace Jellyfin.Api.Controllers args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); } - args += " " + _encodingHelper.GetAudioFilterParam(state, encodingOptions, true); + args += " " + _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true); return args; } - private string GetVideoArguments(StreamState state, EncodingOptions encodingOptions, int startNumber) + /// + /// Gets the video arguments for transcoding. + /// + /// The . + /// The first number in the hls sequence. + /// The command line arguments for video transcoding. + private string GetVideoArguments(StreamState state, int startNumber) { + if (state.VideoStream == null) + { + return string.Empty; + } + if (!state.IsOutputVideo) { return string.Empty; } - var codec = _encodingHelper.GetVideoEncoder(state, encodingOptions); + var codec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); var args = "-codec:v:0 " + codec; - // Prefer hvc1 to hev1 + // Prefer hvc1 to hev1. if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase) || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) @@ -1529,7 +1544,7 @@ namespace Jellyfin.Api.Controllers // args += " -mpegts_m2ts_mode 1"; // } - // See if we can save come cpu cycles by avoiding encoding + // See if we can save come cpu cycles by avoiding encoding. if (EncodingHelper.IsCopyCodec(codec)) { if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) @@ -1553,22 +1568,21 @@ namespace Jellyfin.Api.Controllers state.SegmentLength); var framerate = state.VideoStream?.RealFrameRate; - if (framerate.HasValue) { // This is to make sure keyframe interval is limited to our segment, // as forcing keyframes is not enough. // Example: we encoded half of desired length, then codec detected // scene cut and inserted a keyframe; next forced keyframe would - // be created outside of segment, which breaks seeking - // -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe + // be created outside of segment, which breaks seeking. + // -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe. gopArg = string.Format( CultureInfo.InvariantCulture, " -g {0} -keyint_min {0} -sc_threshold 0", Math.Ceiling(state.SegmentLength * framerate.Value)); } - args += " " + _encodingHelper.GetVideoQualityParam(state, codec, encodingOptions, "veryfast"); + args += " " + _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset); // Unable to force key frames using these encoders, set key frames by GOP. if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase) @@ -1602,16 +1616,15 @@ namespace Jellyfin.Api.Controllers var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; - // This is for graphical subs if (hasGraphicalSubs) { - args += _encodingHelper.GetGraphicalSubtitleParam(state, encodingOptions, codec); + // Graphical subs overlay and resolution params. + args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec); } - - // Add resolution params, if specified else { - args += _encodingHelper.GetOutputSizeParam(state, encodingOptions, codec); + // Resolution params. + args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec); } // -start_at_zero is necessary to use with -ss when seeking, diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs index d7bcf79c1..7676b2331 100644 --- a/Jellyfin.Api/Controllers/VideoHlsController.cs +++ b/Jellyfin.Api/Controllers/VideoHlsController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.IO; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; @@ -37,7 +38,7 @@ namespace Jellyfin.Api.Controllers public class VideoHlsController : BaseJellyfinApiController { private const string DefaultEncoderPreset = "superfast"; - private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls; + private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls; private readonly EncodingHelper _encodingHelper; private readonly IDlnaManager _dlnaManager; @@ -290,30 +291,30 @@ namespace Jellyfin.Api.Controllers _dlnaManager, _deviceManager, _transcodingJobHelper, - TranscodingJobType, + _transcodingJobType, cancellationTokenSource.Token) .ConfigureAwait(false); TranscodingJobDto? job = null; - var playlist = state.OutputFilePath; + var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8"); - if (!System.IO.File.Exists(playlist)) + if (!System.IO.File.Exists(playlistPath)) { - var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlist); + var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath); await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); try { - if (!System.IO.File.Exists(playlist)) + if (!System.IO.File.Exists(playlistPath)) { // If the playlist doesn't already exist, startup ffmpeg try { job = await _transcodingJobHelper.StartFfMpeg( state, - playlist, - GetCommandLineArguments(playlist, state), + playlistPath, + GetCommandLineArguments(playlistPath, state), Request, - TranscodingJobType, + _transcodingJobType, cancellationTokenSource) .ConfigureAwait(false); job.IsLiveOutput = true; @@ -327,7 +328,7 @@ namespace Jellyfin.Api.Controllers minSegments = state.MinSegments; if (minSegments > 0) { - await HlsHelpers.WaitForMinimumSegmentCount(playlist, minSegments, _logger, cancellationTokenSource.Token).ConfigureAwait(false); + await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationTokenSource.Token).ConfigureAwait(false); } } } @@ -337,14 +338,14 @@ namespace Jellyfin.Api.Controllers } } - job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlist, TranscodingJobType); + job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType); if (job != null) { _transcodingJobHelper.OnTranscodeEndRequest(job); } - var playlistText = HlsHelpers.GetLivePlaylistText(playlist, state.SegmentLength); + var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state); return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8")); } @@ -360,14 +361,43 @@ namespace Jellyfin.Api.Controllers var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); var threads = _encodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec); var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions); - var format = !string.IsNullOrWhiteSpace(state.Request.SegmentContainer) ? "." + state.Request.SegmentContainer : ".ts"; - var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + format; + var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty; - var segmentFormat = format.TrimStart('.'); + var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath); + var outputPrefix = Path.Combine(Path.GetDirectoryName(outputPath), outputFileNameWithoutExtension); + var outputExtension = HlsHelpers.GetSegmentFileExtension(state.Request.SegmentContainer); + var outputTsArg = outputPrefix + "%d" + outputExtension; + + var segmentFormat = outputExtension.TrimStart('.'); if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase)) { segmentFormat = "mpegts"; } + else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)) + { + var outputFmp4HeaderArg = string.Empty; + var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + if (isWindows) + { + // on Windows, the path of fmp4 header file needs to be configured + outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\""; + } + else + { + // on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder + outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\""; + } + + segmentFormat = "fmp4" + outputFmp4HeaderArg; + } + else + { + _logger.LogError("Invalid HLS segment container: " + segmentFormat); + } + + var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128 + ? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture) + : "128"; var baseUrlParam = string.Format( CultureInfo.InvariantCulture, @@ -376,20 +406,19 @@ namespace Jellyfin.Api.Controllers return string.Format( CultureInfo.InvariantCulture, - "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {7} -individual_header_trailer 0 -segment_format {8} -segment_list_entry_prefix {9} -segment_list_type m3u8 -segment_start_number 0 -segment_list \"{10}\" -y \"{11}\"", + "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts 0 -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -individual_header_trailer 0 -hls_segment_type {8} -start_number 0 -hls_base_url {9} -hls_playlist_type event -hls_segment_filename \"{10}\" -y \"{11}\"", inputModifier, _encodingHelper.GetInputArgument(state, _encodingOptions), threads, _encodingHelper.GetMapArgs(state), GetVideoArguments(state), GetAudioArguments(state), + maxMuxingQueueSize, state.SegmentLength.ToString(CultureInfo.InvariantCulture), - string.Empty, segmentFormat, baseUrlParam, - outputPath, - outputTsArg) - .Trim(); + outputTsArg, + outputPath).Trim(); } /// @@ -399,14 +428,49 @@ namespace Jellyfin.Api.Controllers /// The command line arguments for audio transcoding. private string GetAudioArguments(StreamState state) { - var codec = _encodingHelper.GetAudioEncoder(state); + if (state.AudioStream == null) + { + return string.Empty; + } - if (EncodingHelper.IsCopyCodec(codec)) + var audioCodec = _encodingHelper.GetAudioEncoder(state); + + if (!state.IsOutputVideo) + { + if (EncodingHelper.IsCopyCodec(audioCodec)) + { + return "-acodec copy -strict -2"; + } + + var audioTranscodeParams = new List(); + + audioTranscodeParams.Add("-acodec " + audioCodec); + + if (state.OutputAudioBitrate.HasValue) + { + audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (state.OutputAudioChannels.HasValue) + { + audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (state.OutputAudioSampleRate.HasValue) + { + audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)); + } + + audioTranscodeParams.Add("-vn"); + return string.Join(' ', audioTranscodeParams); + } + + if (EncodingHelper.IsCopyCodec(audioCodec)) { - return "-codec:a:0 copy"; + return "-codec:a:0 copy -strict -2"; } - var args = "-codec:a:0 " + codec; + var args = "-codec:a:0 " + audioCodec; var channels = state.OutputAudioChannels; @@ -439,6 +503,11 @@ namespace Jellyfin.Api.Controllers /// The command line arguments for video transcoding. private string GetVideoArguments(StreamState state) { + if (state.VideoStream == null) + { + return string.Empty; + } + if (!state.IsOutputVideo) { return string.Empty; @@ -448,17 +517,25 @@ namespace Jellyfin.Api.Controllers var args = "-codec:v:0 " + codec; + // Prefer hvc1 to hev1. + 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)) + { + args += " -tag:v:0 hvc1"; + } + // if (state.EnableMpegtsM2TsMode) // { // args += " -mpegts_m2ts_mode 1"; // } - // See if we can save come cpu cycles by avoiding encoding - if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase)) + // See if we can save come cpu cycles by avoiding encoding. + if (EncodingHelper.IsCopyCodec(codec)) { - // if h264_mp4toannexb is ever added, do not use it for live tv - if (state.VideoStream != null && - !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) + // If h264_mp4toannexb is ever added, do not use it for live tv. + if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) { string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream); if (!string.IsNullOrEmpty(bitStreamArgs)) @@ -469,26 +546,74 @@ namespace Jellyfin.Api.Controllers } else { + var gopArg = string.Empty; var keyFrameArg = string.Format( CultureInfo.InvariantCulture, " -force_key_frames \"expr:gte(t,n_forced*{0})\"", state.SegmentLength.ToString(CultureInfo.InvariantCulture)); - var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var framerate = state.VideoStream?.RealFrameRate; + if (framerate.HasValue) + { + // This is to make sure keyframe interval is limited to our segment, + // as forcing keyframes is not enough. + // Example: we encoded half of desired length, then codec detected + // scene cut and inserted a keyframe; next forced keyframe would + // be created outside of segment, which breaks seeking. + // -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe. + gopArg = string.Format( + CultureInfo.InvariantCulture, + " -g {0} -keyint_min {0} -sc_threshold 0", + Math.Ceiling(state.SegmentLength * framerate.Value)); + } - args += " " + _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset) + keyFrameArg; + args += " " + _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset); - // Add resolution params, if specified - if (!hasGraphicalSubs) + // Unable to force key frames using these encoders, set key frames by GOP. + if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc_amf", StringComparison.OrdinalIgnoreCase)) { - args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec); + args += " " + gopArg; + } + else if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) + { + args += " " + keyFrameArg; + } + else + { + args += " " + keyFrameArg + gopArg; } - // This is for internal graphical subs + // Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now. + if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)) + { + args += " -bf 0"; + } + + var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + if (hasGraphicalSubs) { + // Graphical subs overlay and resolution params. args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec); } + else + { + // Resolution params. + args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec); + } + + if (!(state.SubtitleStream != null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)) + { + args += " -start_at_zero"; + } } args += " -flags -global_header"; diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs index 242496697..0bbe92baa 100644 --- a/Jellyfin.Api/Helpers/HlsHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsHelpers.cs @@ -1,8 +1,10 @@ using System; using System.Globalization; using System.IO; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; @@ -69,25 +71,79 @@ namespace Jellyfin.Api.Helpers } } + /// + /// Gets the extension of segment container. + /// + /// The name of the segment container. + /// The string text of extension. + public static string GetSegmentFileExtension(string? segmentContainer) + { + if (!string.IsNullOrWhiteSpace(segmentContainer)) + { + return "." + segmentContainer; + } + + return ".ts"; + } + + /// + /// Gets the #EXT-X-MAP string. + /// + /// The output path of the file. + /// The . + /// Get a normal string or depends on OS. + /// The string text of #EXT-X-MAP. + public static string GetFmp4InitFileName(string outputPath, StreamState state, bool isOsDepends) + { + var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath); + var outputPrefix = Path.Combine(Path.GetDirectoryName(outputPath), outputFileNameWithoutExtension); + var outputExtension = GetSegmentFileExtension(state.Request.SegmentContainer); + + // on Linux/Unix + // #EXT-X-MAP:URI="prefix-1.mp4" + var fmp4InitFileName = outputFileNameWithoutExtension + "-1" + outputExtension; + if (!isOsDepends) + { + return fmp4InitFileName; + } + + var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + if (isWindows) + { + // on Windows + // #EXT-X-MAP:URI="X:\transcodes\prefix-1.mp4" + fmp4InitFileName = outputPrefix + "-1" + outputExtension; + } + + return fmp4InitFileName; + } + /// /// Gets the hls playlist text. /// /// The path to the playlist file. - /// The segment length. + /// The . /// The playlist text as a string. - public static string GetLivePlaylistText(string path, int segmentLength) + public static string GetLivePlaylistText(string path, StreamState state) { using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); using var reader = new StreamReader(stream); var text = reader.ReadToEnd(); - text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT", StringComparison.InvariantCulture); - - var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture); + var segmentFormat = GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.'); + if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)) + { + var fmp4InitFileName = GetFmp4InitFileName(path, state, true); + var baseUrlParam = string.Format( + CultureInfo.InvariantCulture, + "hls/{0}/", + Path.GetFileNameWithoutExtension(path)); + var newFmp4InitFileName = baseUrlParam + GetFmp4InitFileName(path, state, false); - text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase); - // text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase); + // Replace fMP4 init file URI. + text = text.Replace(fmp4InitFileName, newFmp4InitFileName, StringComparison.InvariantCulture); + } return text; } -- cgit v1.2.3 From 32bb73acbb071a8329c485738176711dac4e1bcb Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Sat, 14 Nov 2020 14:22:15 +0800 Subject: add aac_adtstoasc bitstream filter for mpegts to mp4 conversion --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 29 +++++++++++++++++++--- Jellyfin.Api/Controllers/VideoHlsController.cs | 28 +++++++++++++++++++-- .../MediaEncoding/EncodingHelper.cs | 16 ++++++++++-- 3 files changed, 66 insertions(+), 7 deletions(-) (limited to 'Jellyfin.Api/Controllers/DynamicHlsController.cs') diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index d4ae124b9..10c2d3fec 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1444,7 +1444,19 @@ namespace Jellyfin.Api.Controllers { if (EncodingHelper.IsCopyCodec(audioCodec)) { - return "-acodec copy -strict -2"; + var segmentFormat = HlsHelpers.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.'); + var bitStreamArgs = string.Empty; + + // Apply aac_adtstoasc bitstream filter when media source is in mpegts. + if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase) + && (string.Equals(state.MediaSource.Container, "mpegts", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.MediaSource.Container, "hls", StringComparison.OrdinalIgnoreCase))) + { + bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.AudioStream); + bitStreamArgs = !string.IsNullOrEmpty(bitStreamArgs) ? " " + bitStreamArgs : string.Empty; + } + + return "-acodec copy -strict -2" + bitStreamArgs; } var audioTranscodeParams = new List(); @@ -1473,13 +1485,24 @@ namespace Jellyfin.Api.Controllers if (EncodingHelper.IsCopyCodec(audioCodec)) { var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); + var segmentFormat = HlsHelpers.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.'); + var bitStreamArgs = string.Empty; + + // Apply aac_adtstoasc bitstream filter when media source is in mpegts. + if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase) + && (string.Equals(state.MediaSource.Container, "mpegts", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.MediaSource.Container, "hls", StringComparison.OrdinalIgnoreCase))) + { + bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.AudioStream); + bitStreamArgs = !string.IsNullOrEmpty(bitStreamArgs) ? " " + bitStreamArgs : string.Empty; + } if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec)) { - return "-codec:a:0 copy -strict -2 -copypriorss:a:0 0"; + return "-codec:a:0 copy -strict -2 -copypriorss:a:0 0" + bitStreamArgs; } - return "-codec:a:0 copy -strict -2"; + return "-codec:a:0 copy -strict -2" + bitStreamArgs; } var args = "-codec:a:0 " + audioCodec; diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs index 7b50b47b1..21c042f4b 100644 --- a/Jellyfin.Api/Controllers/VideoHlsController.cs +++ b/Jellyfin.Api/Controllers/VideoHlsController.cs @@ -439,7 +439,19 @@ namespace Jellyfin.Api.Controllers { if (EncodingHelper.IsCopyCodec(audioCodec)) { - return "-acodec copy -strict -2"; + var segmentFormat = HlsHelpers.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.'); + var bitStreamArgs = string.Empty; + + // Apply aac_adtstoasc bitstream filter when media source is in mpegts. + if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase) + && (string.Equals(state.MediaSource.Container, "mpegts", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.MediaSource.Container, "hls", StringComparison.OrdinalIgnoreCase))) + { + bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.AudioStream); + bitStreamArgs = !string.IsNullOrEmpty(bitStreamArgs) ? " " + bitStreamArgs : string.Empty; + } + + return "-acodec copy -strict -2" + bitStreamArgs; } var audioTranscodeParams = new List(); @@ -467,7 +479,19 @@ namespace Jellyfin.Api.Controllers if (EncodingHelper.IsCopyCodec(audioCodec)) { - return "-codec:a:0 copy -strict -2"; + var segmentFormat = HlsHelpers.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.'); + var bitStreamArgs = string.Empty; + + // Apply aac_adtstoasc bitstream filter when media source is in mpegts. + if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase) + && (string.Equals(state.MediaSource.Container, "mpegts", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.MediaSource.Container, "hls", StringComparison.OrdinalIgnoreCase))) + { + bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.AudioStream); + bitStreamArgs = !string.IsNullOrEmpty(bitStreamArgs) ? " " + bitStreamArgs : string.Empty; + } + + return "-acodec copy -strict -2" + bitStreamArgs; } var args = "-codec:a:0 " + audioCodec; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 0bc5308b6..a8252d473 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -596,10 +596,17 @@ namespace MediaBrowser.Controller.MediaEncoding || codec.IndexOf("hevc", StringComparison.OrdinalIgnoreCase) != -1; } - // TODO This is auto inserted into the mpegts mux so it might not be needed - // https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb + public bool IsAAC(MediaStream stream) + { + var codec = stream.Codec ?? string.Empty; + + return codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1; + } + public string GetBitStreamArgs(MediaStream stream) { + // TODO This is auto inserted into the mpegts mux so it might not be needed + // https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb if (IsH264(stream)) { return "-bsf:v h264_mp4toannexb"; @@ -608,6 +615,11 @@ namespace MediaBrowser.Controller.MediaEncoding { return "-bsf:v hevc_mp4toannexb"; } + else if (IsAAC(stream)) + { + // convert adts header(mpegts) to asc header(mp4) + return "-bsf:a aac_adtstoasc"; + } else { return null; -- cgit v1.2.3 From fc89b7e6416ed750725154104b37f32496435555 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Sat, 14 Nov 2020 15:46:17 +0800 Subject: minor changes --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 2 ++ Jellyfin.Api/Controllers/VideoHlsController.cs | 2 ++ 2 files changed, 4 insertions(+) (limited to 'Jellyfin.Api/Controllers/DynamicHlsController.cs') diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 10c2d3fec..643c6073c 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1579,6 +1579,8 @@ namespace Jellyfin.Api.Controllers } } + args += " -start_at_zero"; + // args += " -flags -global_header"; } else diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs index 21c042f4b..4ae61fbd3 100644 --- a/Jellyfin.Api/Controllers/VideoHlsController.cs +++ b/Jellyfin.Api/Controllers/VideoHlsController.cs @@ -567,6 +567,8 @@ namespace Jellyfin.Api.Controllers args += " " + bitStreamArgs; } } + + args += " -start_at_zero"; } else { -- cgit v1.2.3 From 06670351ae6be1d3c47a25fe14b2f253cc6ab3e7 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Sat, 14 Nov 2020 10:19:41 +0000 Subject: Apply suggestions from code review Co-authored-by: BaronGreenback --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 4 ++-- Jellyfin.Api/Controllers/VideoHlsController.cs | 2 +- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) (limited to 'Jellyfin.Api/Controllers/DynamicHlsController.cs') diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 643c6073c..625275171 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1453,7 +1453,7 @@ namespace Jellyfin.Api.Controllers || string.Equals(state.MediaSource.Container, "hls", StringComparison.OrdinalIgnoreCase))) { bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.AudioStream); - bitStreamArgs = !string.IsNullOrEmpty(bitStreamArgs) ? " " + bitStreamArgs : string.Empty; + bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs; } return "-acodec copy -strict -2" + bitStreamArgs; @@ -1494,7 +1494,7 @@ namespace Jellyfin.Api.Controllers || string.Equals(state.MediaSource.Container, "hls", StringComparison.OrdinalIgnoreCase))) { bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.AudioStream); - bitStreamArgs = !string.IsNullOrEmpty(bitStreamArgs) ? " " + bitStreamArgs : string.Empty; + bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs; } if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec)) diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs index 4ae61fbd3..93bb3a903 100644 --- a/Jellyfin.Api/Controllers/VideoHlsController.cs +++ b/Jellyfin.Api/Controllers/VideoHlsController.cs @@ -448,7 +448,7 @@ namespace Jellyfin.Api.Controllers || string.Equals(state.MediaSource.Container, "hls", StringComparison.OrdinalIgnoreCase))) { bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.AudioStream); - bitStreamArgs = !string.IsNullOrEmpty(bitStreamArgs) ? " " + bitStreamArgs : string.Empty; + bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs;; } return "-acodec copy -strict -2" + bitStreamArgs; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index a8252d473..b181a6ee6 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -605,7 +605,7 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetBitStreamArgs(MediaStream stream) { - // TODO This is auto inserted into the mpegts mux so it might not be needed + // TODO This is auto inserted into the mpegts mux so it might not be needed. // https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb if (IsH264(stream)) { @@ -617,7 +617,7 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (IsAAC(stream)) { - // convert adts header(mpegts) to asc header(mp4) + // Convert adts header(mpegts) to asc header(mp4). return "-bsf:a aac_adtstoasc"; } else -- cgit v1.2.3 From f953dd42be87b5188d476f8c8c8c7d39cd395c73 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Mon, 16 Nov 2020 01:09:14 +0800 Subject: remove unused segment option --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 2 +- Jellyfin.Api/Controllers/VideoHlsController.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'Jellyfin.Api/Controllers/DynamicHlsController.cs') diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 625275171..fc13fb873 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1411,7 +1411,7 @@ namespace Jellyfin.Api.Controllers return string.Format( CultureInfo.InvariantCulture, - "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts 0 -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -individual_header_trailer 0 -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"", + "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"", inputModifier, _encodingHelper.GetInputArgument(state, _encodingOptions), threads, diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs index 1ced08617..aebfeab67 100644 --- a/Jellyfin.Api/Controllers/VideoHlsController.cs +++ b/Jellyfin.Api/Controllers/VideoHlsController.cs @@ -406,7 +406,7 @@ namespace Jellyfin.Api.Controllers return string.Format( CultureInfo.InvariantCulture, - "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts 0 -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -individual_header_trailer 0 -hls_segment_type {8} -start_number 0 -hls_base_url {9} -hls_playlist_type event -hls_segment_filename \"{10}\" -y \"{11}\"", + "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number 0 -hls_base_url {9} -hls_playlist_type event -hls_segment_filename \"{10}\" -y \"{11}\"", inputModifier, _encodingHelper.GetInputArgument(state, _encodingOptions), threads, -- cgit v1.2.3 From 547ee885613591021ad5ff5595249b1f8448d742 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 15 Nov 2020 10:58:39 -0700 Subject: Remove api client generator errors --- Jellyfin.Api/Controllers/AudioController.cs | 169 ++++- Jellyfin.Api/Controllers/DynamicHlsController.cs | 4 +- Jellyfin.Api/Controllers/ImageController.cs | 736 +++++++++++++++++++-- Jellyfin.Api/Controllers/InstantMixController.cs | 4 +- Jellyfin.Api/Controllers/ItemsController.cs | 257 ++++++- Jellyfin.Api/Controllers/SubtitleController.cs | 42 +- Jellyfin.Api/Controllers/TrailersController.cs | 1 - .../Controllers/VideoAttachmentsController.cs | 3 +- Jellyfin.Api/Controllers/VideosController.cs | 165 ++++- MediaBrowser.Model/Dlna/StreamBuilder.cs | 2 +- 10 files changed, 1309 insertions(+), 74 deletions(-) (limited to 'Jellyfin.Api/Controllers/DynamicHlsController.cs') diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index d4c6e4af9..330e56614 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -85,15 +85,178 @@ namespace Jellyfin.Api.Controllers /// Optional. The streaming options. /// Audio stream returned. /// A containing the audio file. - [HttpGet("{itemId}/stream.{container:required}", Name = "GetAudioStreamByContainer")] [HttpGet("{itemId}/stream", Name = "GetAudioStream")] - [HttpHead("{itemId}/stream.{container:required}", Name = "HeadAudioStreamByContainer")] [HttpHead("{itemId}/stream", Name = "HeadAudioStream")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesAudioFile] public async Task GetAudioStream( [FromRoute, Required] Guid itemId, - [FromRoute] string? container, + [FromQuery] string? container, + [FromQuery] bool? @static, + [FromQuery] string? @params, + [FromQuery] string? tag, + [FromQuery] string? deviceProfileId, + [FromQuery] string? playSessionId, + [FromQuery] string? segmentContainer, + [FromQuery] int? segmentLength, + [FromQuery] int? minSegments, + [FromQuery] string? mediaSourceId, + [FromQuery] string? deviceId, + [FromQuery] string? audioCodec, + [FromQuery] bool? enableAutoStreamCopy, + [FromQuery] bool? allowVideoStreamCopy, + [FromQuery] bool? allowAudioStreamCopy, + [FromQuery] bool? breakOnNonKeyFrames, + [FromQuery] int? audioSampleRate, + [FromQuery] int? maxAudioBitDepth, + [FromQuery] int? audioBitRate, + [FromQuery] int? audioChannels, + [FromQuery] int? maxAudioChannels, + [FromQuery] string? profile, + [FromQuery] string? level, + [FromQuery] float? framerate, + [FromQuery] float? maxFramerate, + [FromQuery] bool? copyTimestamps, + [FromQuery] long? startTimeTicks, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? videoBitRate, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] int? maxRefFrames, + [FromQuery] int? maxVideoBitDepth, + [FromQuery] bool? requireAvc, + [FromQuery] bool? deInterlace, + [FromQuery] bool? requireNonAnamorphic, + [FromQuery] int? transcodingMaxAudioChannels, + [FromQuery] int? cpuCoreLimit, + [FromQuery] string? liveStreamId, + [FromQuery] bool? enableMpegtsM2TsMode, + [FromQuery] string? videoCodec, + [FromQuery] string? subtitleCodec, + [FromQuery] string? transcodingReasons, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? videoStreamIndex, + [FromQuery] EncodingContext? context, + [FromQuery] Dictionary? streamOptions) + { + StreamingRequestDto streamingRequest = new StreamingRequestDto + { + Id = itemId, + Container = container, + Static = @static ?? true, + Params = @params, + Tag = tag, + DeviceProfileId = deviceProfileId, + PlaySessionId = playSessionId, + SegmentContainer = segmentContainer, + SegmentLength = segmentLength, + MinSegments = minSegments, + MediaSourceId = mediaSourceId, + DeviceId = deviceId, + AudioCodec = audioCodec, + EnableAutoStreamCopy = enableAutoStreamCopy ?? true, + AllowAudioStreamCopy = allowAudioStreamCopy ?? true, + AllowVideoStreamCopy = allowVideoStreamCopy ?? true, + BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, + AudioSampleRate = audioSampleRate, + MaxAudioChannels = maxAudioChannels, + AudioBitRate = audioBitRate, + MaxAudioBitDepth = maxAudioBitDepth, + AudioChannels = audioChannels, + Profile = profile, + Level = level, + Framerate = framerate, + MaxFramerate = maxFramerate, + CopyTimestamps = copyTimestamps ?? true, + StartTimeTicks = startTimeTicks, + Width = width, + Height = height, + VideoBitRate = videoBitRate, + SubtitleStreamIndex = subtitleStreamIndex, + SubtitleMethod = subtitleMethod, + MaxRefFrames = maxRefFrames, + MaxVideoBitDepth = maxVideoBitDepth, + RequireAvc = requireAvc ?? true, + DeInterlace = deInterlace ?? true, + RequireNonAnamorphic = requireNonAnamorphic ?? true, + TranscodingMaxAudioChannels = transcodingMaxAudioChannels, + CpuCoreLimit = cpuCoreLimit, + LiveStreamId = liveStreamId, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + VideoCodec = videoCodec, + SubtitleCodec = subtitleCodec, + TranscodeReasons = transcodingReasons, + AudioStreamIndex = audioStreamIndex, + VideoStreamIndex = videoStreamIndex, + Context = context ?? EncodingContext.Static, + StreamOptions = streamOptions + }; + + return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false); + } + + /// + /// Gets an audio stream. + /// + /// The item id. + /// The audio container. + /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. + /// The streaming parameters. + /// The tag. + /// Optional. The dlna device profile id to utilize. + /// The play session id. + /// The segment container. + /// The segment lenght. + /// The minimum number of segments. + /// The media version id, if playing an alternate version. + /// The device id of the client requesting. Used to stop encoding processes when needed. + /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. + /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. + /// Whether or not to allow copying of the video stream url. + /// Whether or not to allow copying of the audio stream url. + /// Optional. Whether to break on non key frames. + /// Optional. Specify a specific audio sample rate, e.g. 44100. + /// Optional. The maximum audio bit depth. + /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. + /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. + /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. + /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. + /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. + /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. + /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. + /// Optional. The fixed horizontal resolution of the encoded video. + /// Optional. The fixed vertical resolution of the encoded video. + /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. + /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. + /// Optional. Specify the subtitle delivery method. + /// Optional. + /// Optional. The maximum video bit depth. + /// Optional. Whether to require avc. + /// Optional. Whether to deinterlace the video. + /// Optional. Whether to require a non anamporphic stream. + /// Optional. The maximum number of audio channels to transcode. + /// Optional. The limit of how many cpu cores to use. + /// The live stream id. + /// Optional. Whether to enable the MpegtsM2Ts mode. + /// Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv. + /// Optional. Specify a subtitle codec to encode to. + /// Optional. The transcoding reason. + /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. + /// Optional. The index of the video stream to use. If omitted the first video stream will be used. + /// Optional. The . + /// Optional. The streaming options. + /// Audio stream returned. + /// A containing the audio file. + [HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")] + [HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesAudioFile] + public async Task GetAudioStreamByContainer( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index e07690e11..a6b249ccf 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -833,7 +833,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute] string container, + [FromRoute, Required] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, @@ -1004,7 +1004,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute] string container, + [FromRoute, Required] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 4a67c1aed..bb0f27312 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -85,7 +85,6 @@ namespace Jellyfin.Api.Controllers /// User does not have permission to delete the image. /// A . [HttpPost("Users/{userId}/Images/{imageType}")] - [HttpPost("Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -94,7 +93,53 @@ namespace Jellyfin.Api.Controllers public async Task PostUserImage( [FromRoute, Required] Guid userId, [FromRoute, Required] ImageType imageType, - [FromRoute] int? index = null) + [FromQuery] int? index = null) + { + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + { + return Forbid("User is not allowed to update the image."); + } + + var user = _userManager.GetUserById(userId); + await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + + // Handle image/png; charset=utf-8 + var mimeType = Request.ContentType.Split(';').FirstOrDefault(); + var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); + if (user.ProfileImage != null) + { + await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); + } + + user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType))); + + await _providerManager + .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) + .ConfigureAwait(false); + await _userManager.UpdateUserAsync(user).ConfigureAwait(false); + + return NoContent(); + } + + /// + /// Sets the user image. + /// + /// User Id. + /// (Unused) Image type. + /// (Unused) Image index. + /// Image updated. + /// User does not have permission to delete the image. + /// A . + [HttpPost("Users/{userId}/Images/{imageType}/{index}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] + public async Task PostUserImageByIndex( + [FromRoute, Required] Guid userId, + [FromRoute, Required] ImageType imageType, + [FromRoute] int index) { if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) { @@ -131,8 +176,7 @@ namespace Jellyfin.Api.Controllers /// Image deleted. /// User does not have permission to delete the image. /// A . - [HttpDelete("Users/{userId}/Images/{itemType}")] - [HttpDelete("Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")] + [HttpDelete("Users/{userId}/Images/{imageType}")] [Authorize(Policy = Policies.DefaultAuthorization)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] @@ -141,7 +185,46 @@ namespace Jellyfin.Api.Controllers public async Task DeleteUserImage( [FromRoute, Required] Guid userId, [FromRoute, Required] ImageType imageType, - [FromRoute] int? index = null) + [FromQuery] int? index = null) + { + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + { + return Forbid("User is not allowed to delete the image."); + } + + var user = _userManager.GetUserById(userId); + try + { + System.IO.File.Delete(user.ProfileImage.Path); + } + catch (IOException e) + { + _logger.LogError(e, "Error deleting user profile image:"); + } + + await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Delete the user's image. + /// + /// User Id. + /// (Unused) Image type. + /// (Unused) Image index. + /// Image deleted. + /// User does not have permission to delete the image. + /// A . + [HttpDelete("Users/{userId}/Images/{imageType}/{index}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task DeleteUserImageByIndex( + [FromRoute, Required] Guid userId, + [FromRoute, Required] ImageType imageType, + [FromRoute] int index) { if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) { @@ -172,14 +255,13 @@ namespace Jellyfin.Api.Controllers /// Item not found. /// A on success, or a if item not found. [HttpDelete("Items/{itemId}/Images/{imageType}")] - [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "DeleteItemImage_2")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeleteItemImage( [FromRoute, Required] Guid itemId, [FromRoute, Required] ImageType imageType, - [FromRoute] int? imageIndex = null) + [FromQuery] int? imageIndex) { var item = _libraryManager.GetItemById(itemId); if (item == null) @@ -191,25 +273,83 @@ namespace Jellyfin.Api.Controllers return NoContent(); } + /// + /// Delete an item's image. + /// + /// Item id. + /// Image type. + /// The image index. + /// Image deleted. + /// Item not found. + /// A on success, or a if item not found. + [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteItemImageByIndex( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] ImageType imageType, + [FromRoute] int imageIndex) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + await item.DeleteImageAsync(imageType, imageIndex).ConfigureAwait(false); + return NoContent(); + } + /// /// Set item image. /// /// Item id. /// Image type. - /// (Unused) Image index. /// Image saved. /// Item not found. /// A on success, or a if item not found. [HttpPost("Items/{itemId}/Images/{imageType}")] - [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "SetItemImage_2")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] public async Task SetItemImage( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] ImageType imageType) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + // Handle image/png; charset=utf-8 + var mimeType = Request.ContentType.Split(';').FirstOrDefault(); + await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); + await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); + + return NoContent(); + } + + /// + /// Set item image. + /// + /// Item id. + /// Image type. + /// (Unused) Image index. + /// Image saved. + /// Item not found. + /// A on success, or a if item not found. + [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] + public async Task SetItemImageByIndex( [FromRoute, Required] Guid itemId, [FromRoute, Required] ImageType imageType, - [FromRoute] int? imageIndex = null) + [FromRoute] int imageIndex) { var item = _libraryManager.GetItemById(itemId); if (item == null) @@ -349,8 +489,6 @@ namespace Jellyfin.Api.Controllers /// [HttpGet("Items/{itemId}/Images/{imageType}")] [HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")] - [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "GetItemImage_2")] - [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] @@ -371,7 +509,86 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer, - [FromRoute] int? imageIndex = null) + [FromQuery] int? imageIndex) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + return await GetImageInternal( + itemId, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + item, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + .ConfigureAwait(false); + } + + /// + /// Gets the item's image. + /// + /// Item id. + /// Image type. + /// Image index. + /// The maximum image width to return. + /// The maximum image height to return. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. The of the returned image. + /// Optional. Add a played indicator. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")] + [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}", Name = "HeadItemImageByIndex")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetItemImageByIndex( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] ImageType imageType, + [FromRoute] int imageIndex, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] string? tag, + [FromQuery] bool? cropWhitespace, + [FromQuery] ImageFormat? format, + [FromQuery] bool? addPlayedIndicator, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer) { var item = _libraryManager.GetItemById(itemId); if (item == null) @@ -507,8 +724,8 @@ namespace Jellyfin.Api.Controllers /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")] + [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex}")] + [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex}", Name = "HeadArtistImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] @@ -586,8 +803,8 @@ namespace Jellyfin.Api.Controllers /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")] + [HttpGet("Genres/{name}/Images/{imageType}")] + [HttpHead("Genres/{name}/Images/{imageType}", Name = "HeadGenreImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] @@ -608,7 +825,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer, - [FromRoute] int? imageIndex = null) + [FromQuery] int? imageIndex) { var item = _libraryManager.GetGenre(name); if (item == null) @@ -640,10 +857,11 @@ namespace Jellyfin.Api.Controllers } /// - /// Get music genre image by name. + /// Get genre image by name. /// - /// Music genre name. + /// Genre name. /// Image type. + /// Image index. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Determines the output format of the image - original,gif,jpg,png. /// The maximum image width to return. @@ -658,21 +876,21 @@ namespace Jellyfin.Api.Controllers /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. - /// Image index. /// Image stream returned. /// Item not found. /// /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")] + [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex}")] + [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadGenreImageByIndex")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] - public async Task GetMusicGenreImage( + public async Task GetGenreImageByIndex( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, + [FromRoute, Required] int imageIndex, [FromQuery] string tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, @@ -686,10 +904,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, - [FromQuery] string? foregroundLayer, - [FromRoute] int? imageIndex = null) + [FromQuery] string? foregroundLayer) { - var item = _libraryManager.GetMusicGenre(name); + var item = _libraryManager.GetGenre(name); if (item == null) { return NotFound(); @@ -719,9 +936,9 @@ namespace Jellyfin.Api.Controllers } /// - /// Get person image by name. + /// Get music genre image by name. /// - /// Person name. + /// Music genre name. /// Image type. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Determines the output format of the image - original,gif,jpg,png. @@ -744,12 +961,12 @@ namespace Jellyfin.Api.Controllers /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")] + [HttpGet("MusicGenres/{name}/Images/{imageType}")] + [HttpHead("MusicGenres/{name}/Images/{imageType}", Name = "HeadMusicGenreImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] - public async Task GetPersonImage( + public async Task GetMusicGenreImage( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromQuery] string tag, @@ -766,9 +983,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer, - [FromRoute] int? imageIndex = null) + [FromQuery] int? imageIndex) { - var item = _libraryManager.GetPerson(name); + var item = _libraryManager.GetMusicGenre(name); if (item == null) { return NotFound(); @@ -798,10 +1015,11 @@ namespace Jellyfin.Api.Controllers } /// - /// Get studio image by name. + /// Get music genre image by name. /// - /// Studio name. + /// Music genre name. /// Image type. + /// Image index. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Determines the output format of the image - original,gif,jpg,png. /// The maximum image width to return. @@ -816,23 +1034,23 @@ namespace Jellyfin.Api.Controllers /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. - /// Image index. /// Image stream returned. /// Item not found. /// /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")] + [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex}")] + [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadMusicGenreImageByIndex")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] - public async Task GetStudioImage( + public async Task GetMusicGenreImageByIndex( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, - [FromRoute, Required] string tag, - [FromRoute, Required] ImageFormat format, + [FromRoute, Required] int imageIndex, + [FromQuery] string tag, + [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, @@ -844,10 +1062,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, - [FromQuery] string? foregroundLayer, - [FromRoute] int? imageIndex = null) + [FromQuery] string? foregroundLayer) { - var item = _libraryManager.GetStudio(name); + var item = _libraryManager.GetMusicGenre(name); if (item == null) { return NotFound(); @@ -877,9 +1094,9 @@ namespace Jellyfin.Api.Controllers } /// - /// Get user profile image. + /// Get person image by name. /// - /// User id. + /// Person name. /// Image type. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Determines the output format of the image - original,gif,jpg,png. @@ -902,15 +1119,15 @@ namespace Jellyfin.Api.Controllers /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex?}")] - [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")] + [HttpGet("Persons/{name}/Images/{imageType}")] + [HttpHead("Persons/{name}/Images/{imageType}", Name = "HeadPersonImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] - public async Task GetUserImage( - [FromRoute, Required] Guid userId, + public async Task GetPersonImage( + [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, - [FromQuery] string? tag, + [FromQuery] string tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, @@ -924,7 +1141,420 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer, - [FromRoute] int? imageIndex = null) + [FromQuery] int? imageIndex) + { + var item = _libraryManager.GetPerson(name); + if (item == null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + item, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + .ConfigureAwait(false); + } + + /// + /// Get person image by name. + /// + /// Person name. + /// Image type. + /// Image index. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Add a played indicator. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex}")] + [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex}", Name = "HeadPersonImageByIndex")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetPersonImageByIndex( + [FromRoute, Required] string name, + [FromRoute, Required] ImageType imageType, + [FromRoute, Required] int imageIndex, + [FromQuery] string tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] bool? cropWhitespace, + [FromQuery] bool? addPlayedIndicator, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer) + { + var item = _libraryManager.GetPerson(name); + if (item == null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + item, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + .ConfigureAwait(false); + } + + /// + /// Get studio image by name. + /// + /// Studio name. + /// Image type. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Add a played indicator. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image index. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Studios/{name}/Images/{imageType}")] + [HttpHead("Studios/{name}/Images/{imageType}", Name = "HeadStudioImage")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetStudioImage( + [FromRoute, Required] string name, + [FromRoute, Required] ImageType imageType, + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] bool? cropWhitespace, + [FromQuery] bool? addPlayedIndicator, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromQuery] int? imageIndex) + { + var item = _libraryManager.GetStudio(name); + if (item == null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + item, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + .ConfigureAwait(false); + } + + /// + /// Get studio image by name. + /// + /// Studio name. + /// Image type. + /// Image index. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Add a played indicator. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex}")] + [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex}", Name = "HeadStudioImageByIndex")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetStudioImageByIndex( + [FromRoute, Required] string name, + [FromRoute, Required] ImageType imageType, + [FromRoute, Required] int imageIndex, + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] bool? cropWhitespace, + [FromQuery] bool? addPlayedIndicator, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer) + { + var item = _libraryManager.GetStudio(name); + if (item == null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + item, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + .ConfigureAwait(false); + } + + /// + /// Get user profile image. + /// + /// User id. + /// Image type. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Add a played indicator. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image index. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Users/{userId}/Images/{imageType}")] + [HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImage")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetUserImage( + [FromRoute, Required] Guid userId, + [FromRoute, Required] ImageType imageType, + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] bool? cropWhitespace, + [FromQuery] bool? addPlayedIndicator, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromQuery] int? imageIndex) + { + var user = _userManager.GetUserById(userId); + if (user == null) + { + return NotFound(); + } + + var info = new ItemImageInfo + { + Path = user.ProfileImage.Path, + Type = ImageType.Profile, + DateModified = user.ProfileImage.LastModified + }; + + if (width.HasValue) + { + info.Width = width.Value; + } + + if (height.HasValue) + { + info.Height = height.Value; + } + + return await GetImageInternal( + user.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + null, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase), + info) + .ConfigureAwait(false); + } + + /// + /// Get user profile image. + /// + /// User id. + /// Image type. + /// Image index. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Add a played indicator. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")] + [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndex")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetUserImageByIndex( + [FromRoute, Required] Guid userId, + [FromRoute, Required] ImageType imageType, + [FromRoute, Required] int imageIndex, + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] bool? cropWhitespace, + [FromQuery] bool? addPlayedIndicator, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer) { var user = _userManager.GetUserById(userId); if (user == null) diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index d17a26db4..efb0ae19b 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -206,7 +206,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The image types to include in the output. /// Instant playlist returned. /// A with the playlist items. - [HttpGet("Artists/InstantMix")] + [HttpGet("Artists/{id}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetInstantMixFromArtists( [FromRoute, Required] Guid id, @@ -242,7 +242,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The image types to include in the output. /// Instant playlist returned. /// A with the playlist items. - [HttpGet("MusicGenres/InstantMix")] + [HttpGet("MusicGenres/{id}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetInstantMixFromMusicGenres( [FromRoute, Required] Guid id, diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index d8d371ebc..63985c72a 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -60,7 +60,6 @@ namespace Jellyfin.Api.Controllers /// /// Gets items based on a query. /// - /// The user id supplied in the /Users/{uid}/Items. /// The user id supplied as query parameter. /// Optional filter by maximum official rating (PG, PG-13, TV-MA, etc). /// Optional filter by items with theme songs. @@ -143,10 +142,8 @@ namespace Jellyfin.Api.Controllers /// Optional, include image information in output. /// A with the items. [HttpGet("Items")] - [HttpGet("Users/{uId}/Items", Name = "GetItems_2")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetItems( - [FromRoute] Guid? uId, [FromQuery] Guid? userId, [FromQuery] string? maxOfficialRating, [FromQuery] bool? hasThemeSong, @@ -228,9 +225,6 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { - // use user id route parameter over query parameter - userId = uId ?? userId; - var user = userId.HasValue && !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId.Value) : null; @@ -505,6 +499,257 @@ namespace Jellyfin.Api.Controllers return new QueryResult { StartIndex = startIndex.GetValueOrDefault(), TotalRecordCount = result.TotalRecordCount, Items = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user) }; } + /// + /// Gets items based on a query. + /// + /// The user id supplied as query parameter. + /// Optional filter by maximum official rating (PG, PG-13, TV-MA, etc). + /// Optional filter by items with theme songs. + /// Optional filter by items with theme videos. + /// Optional filter by items with subtitles. + /// Optional filter by items with special features. + /// Optional filter by items with trailers. + /// Optional. Return items that are siblings of a supplied item. + /// Optional filter by parent index number. + /// Optional filter by items that have or do not have a parental rating. + /// Optional filter by items that are HD or not. + /// Optional filter by items that are 4K or not. + /// Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted. + /// Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted. + /// Optional filter by items that are missing episodes or not. + /// Optional filter by items that are unaired episodes or not. + /// Optional filter by minimum community rating. + /// Optional filter by minimum critic rating. + /// Optional. The minimum premiere date. Format = ISO. + /// Optional. The minimum last saved date. Format = ISO. + /// Optional. The minimum last saved date for the current user. Format = ISO. + /// Optional. The maximum premiere date. Format = ISO. + /// Optional filter by items that have an overview or not. + /// Optional filter by items that have an imdb id or not. + /// Optional filter by items that have a tmdb id or not. + /// Optional filter by items that have a tvdb id or not. + /// Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// When searching within folders, this determines whether or not the search will be recursive. true/false. + /// Optional. Filter based on a search term. + /// Sort Order - Ascending,Descending. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted. + /// Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted. + /// Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional filter by items that are marked as favorite, or not. + /// Optional filter by MediaType. Allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited. + /// Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime. + /// Optional filter by items that are played, or not. + /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted. + /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted. + /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted. + /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted. + /// Optional, include user data. + /// Optional, the max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. If specified, results will be filtered to include only those containing the specified person. + /// Optional. If specified, results will be filtered to include only those containing the specified person id. + /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited. + /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted. + /// Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted. + /// Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted. + /// Optional. If specified, results will be filtered to include only those containing the specified artist id. + /// Optional. If specified, results will be filtered to include only those containing the specified album artist id. + /// Optional. If specified, results will be filtered to include only those containing the specified contributing artist id. + /// Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted. + /// Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted. + /// Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited. + /// Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted. + /// Optional filter by minimum official rating (PG, PG-13, TV-MA, etc). + /// Optional filter by items that are locked. + /// Optional filter by items that are placeholders. + /// Optional filter by items that have official ratings. + /// Whether or not to hide items behind their boxsets. + /// Optional. Filter by the minimum width of the item. + /// Optional. Filter by the minimum height of the item. + /// Optional. Filter by the maximum width of the item. + /// Optional. Filter by the maximum height of the item. + /// Optional filter by items that are 3D, or not. + /// Optional filter by Series Status. Allows multiple, comma delimeted. + /// Optional filter by items whose name is sorted equally or greater than a given input string. + /// Optional filter by items whose name is sorted equally than a given input string. + /// Optional filter by items whose name is equally or lesser than a given input string. + /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimeted. + /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted. + /// Optional. Enable the total record count. + /// Optional, include image information in output. + /// A with the items. + [HttpGet("Users/{userId}/Items")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetItemsByUserId( + [FromRoute] Guid userId, + [FromQuery] string? maxOfficialRating, + [FromQuery] bool? hasThemeSong, + [FromQuery] bool? hasThemeVideo, + [FromQuery] bool? hasSubtitles, + [FromQuery] bool? hasSpecialFeature, + [FromQuery] bool? hasTrailer, + [FromQuery] string? adjacentTo, + [FromQuery] int? parentIndexNumber, + [FromQuery] bool? hasParentalRating, + [FromQuery] bool? isHd, + [FromQuery] bool? is4K, + [FromQuery] string? locationTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes, + [FromQuery] bool? isMissing, + [FromQuery] bool? isUnaired, + [FromQuery] double? minCommunityRating, + [FromQuery] double? minCriticRating, + [FromQuery] DateTime? minPremiereDate, + [FromQuery] DateTime? minDateLastSaved, + [FromQuery] DateTime? minDateLastSavedForUser, + [FromQuery] DateTime? maxPremiereDate, + [FromQuery] bool? hasOverview, + [FromQuery] bool? hasImdbId, + [FromQuery] bool? hasTmdbId, + [FromQuery] bool? hasTvdbId, + [FromQuery] string? excludeItemIds, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] bool? recursive, + [FromQuery] string? searchTerm, + [FromQuery] string? sortOrder, + [FromQuery] string? parentId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery] string? excludeItemTypes, + [FromQuery] string? includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, + [FromQuery] bool? isFavorite, + [FromQuery] string? mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, + [FromQuery] string? sortBy, + [FromQuery] bool? isPlayed, + [FromQuery] string? genres, + [FromQuery] string? officialRatings, + [FromQuery] string? tags, + [FromQuery] string? years, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery] string? person, + [FromQuery] string? personIds, + [FromQuery] string? personTypes, + [FromQuery] string? studios, + [FromQuery] string? artists, + [FromQuery] string? excludeArtistIds, + [FromQuery] string? artistIds, + [FromQuery] string? albumArtistIds, + [FromQuery] string? contributingArtistIds, + [FromQuery] string? albums, + [FromQuery] string? albumIds, + [FromQuery] string? ids, + [FromQuery] string? videoTypes, + [FromQuery] string? minOfficialRating, + [FromQuery] bool? isLocked, + [FromQuery] bool? isPlaceHolder, + [FromQuery] bool? hasOfficialRating, + [FromQuery] bool? collapseBoxSetItems, + [FromQuery] int? minWidth, + [FromQuery] int? minHeight, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] bool? is3D, + [FromQuery] string? seriesStatus, + [FromQuery] string? nameStartsWithOrGreater, + [FromQuery] string? nameStartsWith, + [FromQuery] string? nameLessThan, + [FromQuery] string? studioIds, + [FromQuery] string? genreIds, + [FromQuery] bool enableTotalRecordCount = true, + [FromQuery] bool? enableImages = true) + { + return GetItems( + userId, + maxOfficialRating, + hasThemeSong, + hasThemeVideo, + hasSubtitles, + hasSpecialFeature, + hasTrailer, + adjacentTo, + parentIndexNumber, + hasParentalRating, + isHd, + is4K, + locationTypes, + excludeLocationTypes, + isMissing, + isUnaired, + minCommunityRating, + minCriticRating, + minPremiereDate, + minDateLastSaved, + minDateLastSavedForUser, + maxPremiereDate, + hasOverview, + hasImdbId, + hasTmdbId, + hasTvdbId, + excludeItemIds, + startIndex, + limit, + recursive, + searchTerm, + sortOrder, + parentId, + fields, + excludeItemTypes, + includeItemTypes, + filters, + isFavorite, + mediaTypes, + imageTypes, + sortBy, + isPlayed, + genres, + officialRatings, + tags, + years, + enableUserData, + imageTypeLimit, + enableImageTypes, + person, + personIds, + personTypes, + studios, + artists, + excludeArtistIds, + artistIds, + albumArtistIds, + contributingArtistIds, + albums, + albumIds, + ids, + videoTypes, + minOfficialRating, + isLocked, + isPlaceHolder, + hasOfficialRating, + collapseBoxSetItems, + minWidth, + minHeight, + maxWidth, + maxHeight, + is3D, + seriesStatus, + nameStartsWithOrGreater, + nameStartsWith, + nameLessThan, + studioIds, + genreIds, + enableTotalRecordCount, + enableImages); + } + /// /// Gets items based on a query. /// diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index a01ae31a0..dcb8e803b 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -193,7 +193,6 @@ namespace Jellyfin.Api.Controllers /// File returned. /// A with the subtitle file. [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")] - [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesFile("text/*")] public async Task GetSubtitle( @@ -204,7 +203,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] long? endPositionTicks, [FromQuery] bool copyTimestamps = false, [FromQuery] bool addVttTimeMap = false, - [FromRoute] long startPositionTicks = 0) + [FromQuery] long startPositionTicks = 0) { if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase)) { @@ -249,6 +248,43 @@ namespace Jellyfin.Api.Controllers MimeTypes.GetMimeType("file." + format)); } + /// + /// Gets subtitles in a specified format. + /// + /// The item id. + /// The media source id. + /// The subtitle stream index. + /// Optional. The start position of the subtitle in ticks. + /// The format of the returned subtitle. + /// Optional. The end position of the subtitle in ticks. + /// Optional. Whether to copy the timestamps. + /// Optional. Whether to add a VTT time map. + /// File returned. + /// A with the subtitle file. + [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesFile("text/*")] + public Task GetSubtitleWithTicks( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] string mediaSourceId, + [FromRoute, Required] int index, + [FromRoute, Required] long startPositionTicks, + [FromRoute, Required] string format, + [FromQuery] long? endPositionTicks, + [FromQuery] bool copyTimestamps = false, + [FromQuery] bool addVttTimeMap = false) + { + return GetSubtitle( + itemId, + mediaSourceId, + index, + format, + endPositionTicks, + copyTimestamps, + addVttTimeMap, + startPositionTicks); + } + /// /// Gets an HLS subtitle playlist. /// @@ -335,6 +371,7 @@ namespace Jellyfin.Api.Controllers /// Subtitle uploaded. /// A . [HttpPost("Videos/{itemId}/Subtitles")] + [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task UploadSubtitle( [FromRoute, Required] Guid itemId, [FromBody, Required] UploadSubtitleDto body) @@ -446,6 +483,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("FallbackFont/Fonts/{name}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesFile("font/*")] public ActionResult GetFallbackFont([FromRoute, Required] string name) { var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index d78adcbcd..ffdbb15df 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -197,7 +197,6 @@ namespace Jellyfin.Api.Controllers return _itemsController .GetItems( - userId, userId, maxOfficialRating, hasThemeSong, diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs index 418c0c123..c2bb0dfff 100644 --- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs +++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.Net.Mime; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; @@ -43,7 +44,7 @@ namespace Jellyfin.Api.Controllers /// Video or attachment not found. /// An containing the attachment stream on success, or a if the attachment could not be found. [HttpGet("{videoId}/{mediaSourceId}/Attachments/{index}")] - [Produces(MediaTypeNames.Application.Octet)] + [ProducesFile(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetAttachment( diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 4de7aac71..1a23076f1 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -326,15 +326,13 @@ namespace Jellyfin.Api.Controllers /// Optional. The streaming options. /// Video stream returned. /// A containing the audio file. - [HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetVideoStreamWithExt")] [HttpGet("{itemId}/stream")] - [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStreamWithExt")] [HttpHead("{itemId}/stream", Name = "HeadVideoStream")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesVideoFile] public async Task GetVideoStream( [FromRoute, Required] Guid itemId, - [FromRoute] string? container, + [FromQuery] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, @@ -529,5 +527,166 @@ namespace Jellyfin.Api.Controllers _transcodingJobType, cancellationTokenSource).ConfigureAwait(false); } + + /// + /// Gets a video stream. + /// + /// The item id. + /// The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. + /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. + /// The streaming parameters. + /// The tag. + /// Optional. The dlna device profile id to utilize. + /// The play session id. + /// The segment container. + /// The segment lenght. + /// The minimum number of segments. + /// The media version id, if playing an alternate version. + /// The device id of the client requesting. Used to stop encoding processes when needed. + /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. + /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. + /// Whether or not to allow copying of the video stream url. + /// Whether or not to allow copying of the audio stream url. + /// Optional. Whether to break on non key frames. + /// Optional. Specify a specific audio sample rate, e.g. 44100. + /// Optional. The maximum audio bit depth. + /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. + /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. + /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. + /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. + /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. + /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. + /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. + /// Optional. The fixed horizontal resolution of the encoded video. + /// Optional. The fixed vertical resolution of the encoded video. + /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. + /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. + /// Optional. Specify the subtitle delivery method. + /// Optional. + /// Optional. The maximum video bit depth. + /// Optional. Whether to require avc. + /// Optional. Whether to deinterlace the video. + /// Optional. Whether to require a non anamporphic stream. + /// Optional. The maximum number of audio channels to transcode. + /// Optional. The limit of how many cpu cores to use. + /// The live stream id. + /// Optional. Whether to enable the MpegtsM2Ts mode. + /// Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv. + /// Optional. Specify a subtitle codec to encode to. + /// Optional. The transcoding reason. + /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. + /// Optional. The index of the video stream to use. If omitted the first video stream will be used. + /// Optional. The . + /// Optional. The streaming options. + /// Video stream returned. + /// A containing the audio file. + [HttpGet("{itemId}/{stream=stream}.{container}")] + [HttpHead("{itemId}/{stream=stream}.{container}", Name = "HeadVideoStreamByContainer")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesVideoFile] + public Task GetVideoStreamByContainer( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] string container, + [FromQuery] bool? @static, + [FromQuery] string? @params, + [FromQuery] string? tag, + [FromQuery] string? deviceProfileId, + [FromQuery] string? playSessionId, + [FromQuery] string? segmentContainer, + [FromQuery] int? segmentLength, + [FromQuery] int? minSegments, + [FromQuery] string? mediaSourceId, + [FromQuery] string? deviceId, + [FromQuery] string? audioCodec, + [FromQuery] bool? enableAutoStreamCopy, + [FromQuery] bool? allowVideoStreamCopy, + [FromQuery] bool? allowAudioStreamCopy, + [FromQuery] bool? breakOnNonKeyFrames, + [FromQuery] int? audioSampleRate, + [FromQuery] int? maxAudioBitDepth, + [FromQuery] int? audioBitRate, + [FromQuery] int? audioChannels, + [FromQuery] int? maxAudioChannels, + [FromQuery] string? profile, + [FromQuery] string? level, + [FromQuery] float? framerate, + [FromQuery] float? maxFramerate, + [FromQuery] bool? copyTimestamps, + [FromQuery] long? startTimeTicks, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? videoBitRate, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] int? maxRefFrames, + [FromQuery] int? maxVideoBitDepth, + [FromQuery] bool? requireAvc, + [FromQuery] bool? deInterlace, + [FromQuery] bool? requireNonAnamorphic, + [FromQuery] int? transcodingMaxAudioChannels, + [FromQuery] int? cpuCoreLimit, + [FromQuery] string? liveStreamId, + [FromQuery] bool? enableMpegtsM2TsMode, + [FromQuery] string? videoCodec, + [FromQuery] string? subtitleCodec, + [FromQuery] string? transcodingReasons, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? videoStreamIndex, + [FromQuery] EncodingContext context, + [FromQuery] Dictionary streamOptions) + { + return GetVideoStream( + itemId, + container, + @static, + @params, + tag, + deviceProfileId, + playSessionId, + segmentContainer, + segmentLength, + minSegments, + mediaSourceId, + deviceId, + audioCodec, + enableAutoStreamCopy, + allowVideoStreamCopy, + allowAudioStreamCopy, + breakOnNonKeyFrames, + audioSampleRate, + maxAudioBitDepth, + audioBitRate, + audioChannels, + maxAudioChannels, + profile, + level, + framerate, + maxFramerate, + copyTimestamps, + startTimeTicks, + width, + height, + videoBitRate, + subtitleStreamIndex, + subtitleMethod, + maxRefFrames, + maxVideoBitDepth, + requireAvc, + deInterlace, + requireNonAnamorphic, + transcodingMaxAudioChannels, + cpuCoreLimit, + liveStreamId, + enableMpegtsM2TsMode, + videoCodec, + subtitleCodec, + transcodingReasons, + audioStreamIndex, + videoStreamIndex, + context, + streamOptions); + } } } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 13234c381..4959a9b92 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1647,7 +1647,7 @@ namespace MediaBrowser.Model.Dlna // strip spaces to avoid having to encode var values = value - .Split('|', StringSplitOptions.RemoveEmptyEntries); + .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); if (condition.Condition == ProfileConditionType.Equals || condition.Condition == ProfileConditionType.EqualsAny) { -- cgit v1.2.3 From 51dab0958d11e142f582da9c46a8679335b99dda Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Thu, 19 Nov 2020 22:46:02 +0800 Subject: changes per suggestions --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 91 ++------- Jellyfin.Api/Controllers/VideoHlsController.cs | 90 ++------ Jellyfin.Api/Helpers/HlsHelpers.cs | 20 +- .../MediaEncoding/EncodingHelper.cs | 227 +++++++++++++++------ 4 files changed, 193 insertions(+), 235 deletions(-) (limited to 'Jellyfin.Api/Controllers/DynamicHlsController.cs') diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 28357091b..90c2a6b58 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -43,7 +43,7 @@ namespace Jellyfin.Api.Controllers public class DynamicHlsController : BaseJellyfinApiController { private const string DefaultEncoderPreset = "veryfast"; - private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls; + private const TranscodingJobType _transcodingJobType = TranscodingJobType.Hls; private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; @@ -1446,58 +1446,38 @@ namespace Jellyfin.Api.Controllers { if (EncodingHelper.IsCopyCodec(audioCodec)) { - var segmentFormat = HlsHelpers.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.'); - var bitStreamArgs = string.Empty; - - // Apply aac_adtstoasc bitstream filter when media source is in mpegts. - if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase) - && (string.Equals(state.MediaSource.Container, "mpegts", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.MediaSource.Container, "hls", StringComparison.OrdinalIgnoreCase))) - { - bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.AudioStream); - bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs; - } + var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); return "-acodec copy -strict -2" + bitStreamArgs; } - var audioTranscodeParams = new List(); + var audioTranscodeParams = string.Empty; - audioTranscodeParams.Add("-acodec " + audioCodec); + audioTranscodeParams += "-acodec " + audioCodec; if (state.OutputAudioBitrate.HasValue) { - audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture)); + audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture); } if (state.OutputAudioChannels.HasValue) { - audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture)); + audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture); } if (state.OutputAudioSampleRate.HasValue) { - audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)); + audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); } - audioTranscodeParams.Add("-vn"); - return string.Join(' ', audioTranscodeParams); + audioTranscodeParams += " -vn"; + return audioTranscodeParams; } if (EncodingHelper.IsCopyCodec(audioCodec)) { var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); - var segmentFormat = HlsHelpers.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.'); - var bitStreamArgs = string.Empty; - - // Apply aac_adtstoasc bitstream filter when media source is in mpegts. - if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase) - && (string.Equals(state.MediaSource.Container, "mpegts", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.MediaSource.Container, "hls", StringComparison.OrdinalIgnoreCase))) - { - bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.AudioStream); - bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs; - } + var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec)) { @@ -1528,7 +1508,7 @@ namespace Jellyfin.Api.Controllers args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); } - args += " " + _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true); + args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true); return args; } @@ -1574,7 +1554,7 @@ namespace Jellyfin.Api.Controllers { if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) { - string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream); + string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream); if (!string.IsNullOrEmpty(bitStreamArgs)) { args += " " + bitStreamArgs; @@ -1587,51 +1567,10 @@ namespace Jellyfin.Api.Controllers } else { - var gopArg = string.Empty; - var keyFrameArg = string.Format( - CultureInfo.InvariantCulture, - " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"", - startNumber * state.SegmentLength, - state.SegmentLength); - - var framerate = state.VideoStream?.RealFrameRate; - if (framerate.HasValue) - { - // This is to make sure keyframe interval is limited to our segment, - // as forcing keyframes is not enough. - // Example: we encoded half of desired length, then codec detected - // scene cut and inserted a keyframe; next forced keyframe would - // be created outside of segment, which breaks seeking. - // -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe. - gopArg = string.Format( - CultureInfo.InvariantCulture, - " -g {0} -keyint_min {0} -sc_threshold 0", - Math.Ceiling(state.SegmentLength * framerate.Value)); - } - - args += " " + _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset); + args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset); - // Unable to force key frames using these encoders, set key frames by GOP. - if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "hevc_amf", StringComparison.OrdinalIgnoreCase)) - { - args += " " + gopArg; - } - else if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) - { - args += " " + keyFrameArg; - } - else - { - args += " " + keyFrameArg + gopArg; - } + // Set the key frame params for video encoding to match the hls segment time. + args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, false, startNumber); // Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now. if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)) diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs index b03402c1b..bb8026543 100644 --- a/Jellyfin.Api/Controllers/VideoHlsController.cs +++ b/Jellyfin.Api/Controllers/VideoHlsController.cs @@ -367,7 +367,7 @@ namespace Jellyfin.Api.Controllers var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)); var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath); var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension); - var outputExtension = HlsHelpers.GetSegmentFileExtension(state.Request.SegmentContainer); + var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer); var outputTsArg = outputPrefix + "%d" + outputExtension; var segmentFormat = outputExtension.TrimStart('.'); @@ -441,57 +441,37 @@ namespace Jellyfin.Api.Controllers { if (EncodingHelper.IsCopyCodec(audioCodec)) { - var segmentFormat = HlsHelpers.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.'); - var bitStreamArgs = string.Empty; - - // Apply aac_adtstoasc bitstream filter when media source is in mpegts. - if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase) - && (string.Equals(state.MediaSource.Container, "mpegts", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.MediaSource.Container, "hls", StringComparison.OrdinalIgnoreCase))) - { - bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.AudioStream); - bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs; - } + var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); return "-acodec copy -strict -2" + bitStreamArgs; } - var audioTranscodeParams = new List(); + var audioTranscodeParams = string.Empty; - audioTranscodeParams.Add("-acodec " + audioCodec); + audioTranscodeParams += "-acodec " + audioCodec; if (state.OutputAudioBitrate.HasValue) { - audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture)); + audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture); } if (state.OutputAudioChannels.HasValue) { - audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture)); + audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture); } if (state.OutputAudioSampleRate.HasValue) { - audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)); + audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); } - audioTranscodeParams.Add("-vn"); - return string.Join(' ', audioTranscodeParams); + audioTranscodeParams += " -vn"; + return audioTranscodeParams; } if (EncodingHelper.IsCopyCodec(audioCodec)) { - var segmentFormat = HlsHelpers.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.'); - var bitStreamArgs = string.Empty; - - // Apply aac_adtstoasc bitstream filter when media source is in mpegts. - if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase) - && (string.Equals(state.MediaSource.Container, "mpegts", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.MediaSource.Container, "hls", StringComparison.OrdinalIgnoreCase))) - { - bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.AudioStream); - bitStreamArgs = !string.IsNullOrEmpty(bitStreamArgs) ? " " + bitStreamArgs : string.Empty; - } + var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); return "-acodec copy -strict -2" + bitStreamArgs; } @@ -517,7 +497,7 @@ namespace Jellyfin.Api.Controllers args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); } - args += " " + _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true); + args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true); return args; } @@ -563,7 +543,7 @@ namespace Jellyfin.Api.Controllers // If h264_mp4toannexb is ever added, do not use it for live tv. if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) { - string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream); + string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream); if (!string.IsNullOrEmpty(bitStreamArgs)) { args += " " + bitStreamArgs; @@ -574,50 +554,10 @@ namespace Jellyfin.Api.Controllers } else { - var gopArg = string.Empty; - var keyFrameArg = string.Format( - CultureInfo.InvariantCulture, - " -force_key_frames \"expr:gte(t,n_forced*{0})\"", - state.SegmentLength.ToString(CultureInfo.InvariantCulture)); - - var framerate = state.VideoStream?.RealFrameRate; - if (framerate.HasValue) - { - // This is to make sure keyframe interval is limited to our segment, - // as forcing keyframes is not enough. - // Example: we encoded half of desired length, then codec detected - // scene cut and inserted a keyframe; next forced keyframe would - // be created outside of segment, which breaks seeking. - // -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe. - gopArg = string.Format( - CultureInfo.InvariantCulture, - " -g {0} -keyint_min {0} -sc_threshold 0", - Math.Ceiling(state.SegmentLength * framerate.Value)); - } - - args += " " + _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset); + args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset); - // Unable to force key frames using these encoders, set key frames by GOP. - if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "hevc_amf", StringComparison.OrdinalIgnoreCase)) - { - args += " " + gopArg; - } - else if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) - { - args += " " + keyFrameArg; - } - else - { - args += " " + keyFrameArg + gopArg; - } + // Set the key frame params for video encoding to match the hls segment time. + args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, true, null); // Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now. if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)) diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs index 1c874e4bd..18e23fb5c 100644 --- a/Jellyfin.Api/Helpers/HlsHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsHelpers.cs @@ -5,6 +5,7 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Models.StreamingDtos; +using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; @@ -76,21 +77,6 @@ namespace Jellyfin.Api.Helpers } } - /// - /// Gets the extension of segment container. - /// - /// The name of the segment container. - /// The string text of extension. - public static string GetSegmentFileExtension(string? segmentContainer) - { - if (!string.IsNullOrWhiteSpace(segmentContainer)) - { - return "." + segmentContainer; - } - - return ".ts"; - } - /// /// Gets the #EXT-X-MAP string. /// @@ -103,7 +89,7 @@ namespace Jellyfin.Api.Helpers var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)); var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath); var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension); - var outputExtension = GetSegmentFileExtension(state.Request.SegmentContainer); + var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer); // on Linux/Unix // #EXT-X-MAP:URI="prefix-1.mp4" @@ -137,7 +123,7 @@ namespace Jellyfin.Api.Helpers var text = reader.ReadToEnd(); - var segmentFormat = GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.'); + var segmentFormat = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.'); if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)) { var fmp4InitFileName = GetFmp4InitFileName(path, state, true); diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index b181a6ee6..a2e3318f3 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -580,7 +580,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// /// The stream. /// true if the specified stream is H264; otherwise, false. - public bool IsH264(MediaStream stream) + public static bool IsH264(MediaStream stream) { var codec = stream.Codec ?? string.Empty; @@ -588,7 +588,7 @@ namespace MediaBrowser.Controller.MediaEncoding || codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1; } - public bool IsH265(MediaStream stream) + public static bool IsH265(MediaStream stream) { var codec = stream.Codec ?? string.Empty; @@ -596,14 +596,14 @@ namespace MediaBrowser.Controller.MediaEncoding || codec.IndexOf("hevc", StringComparison.OrdinalIgnoreCase) != -1; } - public bool IsAAC(MediaStream stream) + public static bool IsAAC(MediaStream stream) { var codec = stream.Codec ?? string.Empty; return codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1; } - public string GetBitStreamArgs(MediaStream stream) + public static string GetBitStreamArgs(MediaStream stream) { // TODO This is auto inserted into the mpegts mux so it might not be needed. // https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb @@ -626,6 +626,33 @@ namespace MediaBrowser.Controller.MediaEncoding } } + public static string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer) + { + var bitStreamArgs = string.Empty; + var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.'); + + // Apply aac_adtstoasc bitstream filter when media source is in mpegts. + if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase) + && (string.Equals(mediaSourceContainer, "mpegts", StringComparison.OrdinalIgnoreCase) + || string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase))) + { + bitStreamArgs = GetBitStreamArgs(state.AudioStream); + bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs; + } + + return bitStreamArgs; + } + + public static string GetSegmentFileExtension(string segmentContainer) + { + if (!string.IsNullOrWhiteSpace(segmentContainer)) + { + return "." + segmentContainer; + } + + return ".ts"; + } + public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec) { var bitrate = state.OutputVideoBitrate; @@ -799,6 +826,72 @@ namespace MediaBrowser.Controller.MediaEncoding return null; } + public string GetHlsVideoKeyFrameArguments( + EncodingJobInfo state, + string codec, + int segmentLength, + bool isEventPlaylist, + int? startNumber) + { + var args = string.Empty; + var gopArg = string.Empty; + var keyFrameArg = string.Empty; + if (isEventPlaylist) + { + keyFrameArg = string.Format( + CultureInfo.InvariantCulture, + " -force_key_frames:0 \"expr:gte(t,n_forced*{0})\"", + segmentLength); + } + else if (startNumber.HasValue) + { + keyFrameArg = string.Format( + CultureInfo.InvariantCulture, + " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"", + startNumber.Value * segmentLength, + segmentLength); + } + + var framerate = state.VideoStream?.RealFrameRate; + if (framerate.HasValue) + { + // This is to make sure keyframe interval is limited to our segment, + // as forcing keyframes is not enough. + // Example: we encoded half of desired length, then codec detected + // scene cut and inserted a keyframe; next forced keyframe would + // be created outside of segment, which breaks seeking. + // -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe. + gopArg = string.Format( + CultureInfo.InvariantCulture, + " -g:v:0 {0} -keyint_min:v:0 {0} -sc_threshold:v:0 0", + Math.Ceiling(segmentLength * framerate.Value)); + } + + // Unable to force key frames using these encoders, set key frames by GOP. + if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc_amf", StringComparison.OrdinalIgnoreCase)) + { + args += gopArg; + } + else if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) + { + args += " " + keyFrameArg; + } + else + { + args += " " + keyFrameArg + gopArg; + } + + return args; + } + /// /// Gets the video bitrate to specify on the command line. /// @@ -806,6 +899,47 @@ namespace MediaBrowser.Controller.MediaEncoding { var param = string.Empty; + if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) + { + param += " -pix_fmt yuv420p"; + } + + if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) + { + var videoStream = state.VideoStream; + var isColorDepth10 = IsColorDepth10(state); + + if (isColorDepth10 + && _mediaEncoder.SupportsHwaccel("opencl") + && encodingOptions.EnableTonemapping + && !string.IsNullOrEmpty(videoStream.VideoRange) + && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase)) + { + param += " -pix_fmt nv12"; + } + else + { + param += " -pix_fmt yuv420p"; + } + } + + if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) + { + param += " -pix_fmt nv21"; + } + var isVc1 = state.VideoStream != null && string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase); var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase); @@ -814,11 +948,11 @@ namespace MediaBrowser.Controller.MediaEncoding { if (!string.IsNullOrEmpty(encodingOptions.EncoderPreset)) { - param += "-preset " + encodingOptions.EncoderPreset; + param += " -preset " + encodingOptions.EncoderPreset; } else { - param += "-preset " + defaultPreset; + param += " -preset " + defaultPreset; } int encodeCrf = encodingOptions.H264Crf; @@ -849,11 +983,11 @@ namespace MediaBrowser.Controller.MediaEncoding if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparer.OrdinalIgnoreCase)) { - param += "-preset " + encodingOptions.EncoderPreset; + param += " -preset " + encodingOptions.EncoderPreset; } else { - param += "-preset 7"; + param += " -preset 7"; } param += " -look_ahead 0"; @@ -866,16 +1000,16 @@ namespace MediaBrowser.Controller.MediaEncoding { case "veryslow": - param += "-preset slow"; // lossless is only supported on maxwell and newer(2014+) + param += " -preset slow"; // lossless is only supported on maxwell and newer(2014+) break; case "slow": case "slower": - param += "-preset slow"; + param += " -preset slow"; break; case "medium": - param += "-preset medium"; + param += " -preset medium"; break; case "fast": @@ -883,11 +1017,11 @@ namespace MediaBrowser.Controller.MediaEncoding case "veryfast": case "superfast": case "ultrafast": - param += "-preset fast"; + param += " -preset fast"; break; default: - param += "-preset default"; + param += " -preset default"; break; } } @@ -899,11 +1033,11 @@ namespace MediaBrowser.Controller.MediaEncoding case "veryslow": case "slow": case "slower": - param += "-quality quality"; + param += " -quality quality"; break; case "medium": - param += "-quality balanced"; + param += " -quality balanced"; break; case "fast": @@ -911,11 +1045,11 @@ namespace MediaBrowser.Controller.MediaEncoding case "veryfast": case "superfast": case "ultrafast": - param += "-quality speed"; + param += " -quality speed"; break; default: - param += "-quality speed"; + param += " -quality speed"; break; } @@ -957,7 +1091,7 @@ namespace MediaBrowser.Controller.MediaEncoding profileScore = Math.Min(profileScore, 2); // http://www.webmproject.org/docs/encoder-parameters/ - param += string.Format(CultureInfo.InvariantCulture, "-speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}", + param += string.Format(CultureInfo.InvariantCulture, " -speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}", profileScore.ToString(_usCulture), crf, qmin, @@ -965,15 +1099,15 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (string.Equals(videoEncoder, "mpeg4", StringComparison.OrdinalIgnoreCase)) { - param += "-mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2"; + param += " -mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2"; } else if (string.Equals(videoEncoder, "wmv2", StringComparison.OrdinalIgnoreCase)) // asf/wmv { - param += "-qmin 2"; + param += " -qmin 2"; } else if (string.Equals(videoEncoder, "msmpeg4", StringComparison.OrdinalIgnoreCase)) { - param += "-mbd 2"; + param += " -mbd 2"; } param += GetVideoBitrateParam(state, videoEncoder); @@ -1035,7 +1169,7 @@ namespace MediaBrowser.Controller.MediaEncoding && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) { // not supported by h264_omx - param += " -profile:v " + profile; + param += " -profile:v:0 " + profile; } } @@ -1091,47 +1225,6 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -x265-params:0 no-info=1"; } - if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) - && !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) - && !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) - && !string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) - && !string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) - && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase) - && !string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase) - && !string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase) - && !string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) - && !string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) - { - param = "-pix_fmt yuv420p " + param; - } - - if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) - { - var videoStream = state.VideoStream; - var isColorDepth10 = IsColorDepth10(state); - - if (isColorDepth10 - && _mediaEncoder.SupportsHwaccel("opencl") - && encodingOptions.EnableTonemapping - && !string.IsNullOrEmpty(videoStream.VideoRange) - && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase)) - { - param = "-pix_fmt nv12 " + param; - } - else - { - param = "-pix_fmt yuv420p " + param; - } - } - - if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) - { - param = "-pix_fmt nv21 " + param; - } - return param; } @@ -1521,7 +1614,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (filters.Count > 0) { - return "-af \"" + string.Join(",", filters) + "\""; + return " -af \"" + string.Join(",", filters) + "\""; } return string.Empty; @@ -3378,7 +3471,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.RunTimeTicks.HasValue && state.BaseRequest.CopyTimestamps) { - args += " -copyts -avoid_negative_ts 0 -start_at_zero"; + args += " -copyts -avoid_negative_ts disabled -start_at_zero"; } if (!state.RunTimeTicks.HasValue) @@ -3426,7 +3519,7 @@ namespace MediaBrowser.Controller.MediaEncoding args += " -copyts"; } - args += " -avoid_negative_ts 0"; + args += " -avoid_negative_ts disabled"; if (!(state.SubtitleStream != null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)) { @@ -3490,7 +3583,7 @@ namespace MediaBrowser.Controller.MediaEncoding args += " -ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture); } - args += " " + GetAudioFilterParam(state, encodingOptions, false); + args += GetAudioFilterParam(state, encodingOptions, false); return args; } -- cgit v1.2.3 From ca33ace3e97a85c01848db072708a4e2cf0b251a Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Fri, 20 Nov 2020 09:56:08 +0800 Subject: rename --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 22 +++++++++++----------- Jellyfin.Api/Controllers/VideoHlsController.cs | 8 ++++---- 2 files changed, 15 insertions(+), 15 deletions(-) (limited to 'Jellyfin.Api/Controllers/DynamicHlsController.cs') diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 90c2a6b58..856c470c2 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -43,7 +43,7 @@ namespace Jellyfin.Api.Controllers public class DynamicHlsController : BaseJellyfinApiController { private const string DefaultEncoderPreset = "veryfast"; - private const TranscodingJobType _transcodingJobType = TranscodingJobType.Hls; + private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls; private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; @@ -276,7 +276,7 @@ namespace Jellyfin.Api.Controllers EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming }; - return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); + return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); } /// @@ -443,7 +443,7 @@ namespace Jellyfin.Api.Controllers EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming }; - return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); + return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); } /// @@ -1133,7 +1133,7 @@ namespace Jellyfin.Api.Controllers _dlnaManager, _deviceManager, _transcodingJobHelper, - _transcodingJobType, + TranscodingJobType, cancellationTokenSource.Token) .ConfigureAwait(false); @@ -1218,7 +1218,7 @@ namespace Jellyfin.Api.Controllers _dlnaManager, _deviceManager, _transcodingJobHelper, - _transcodingJobType, + TranscodingJobType, cancellationTokenSource.Token) .ConfigureAwait(false); @@ -1232,7 +1232,7 @@ namespace Jellyfin.Api.Controllers if (System.IO.File.Exists(segmentPath)) { - job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType); + job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); _logger.LogDebug("returning {0} [it exists, try 1]", segmentPath); return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false); } @@ -1246,7 +1246,7 @@ namespace Jellyfin.Api.Controllers { if (System.IO.File.Exists(segmentPath)) { - job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType); + job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); transcodingLock.Release(); released = true; _logger.LogDebug("returning {0} [it exists, try 2]", segmentPath); @@ -1300,7 +1300,7 @@ namespace Jellyfin.Api.Controllers playlistPath, GetCommandLineArguments(playlistPath, state, true, segmentId), Request, - _transcodingJobType, + TranscodingJobType, cancellationTokenSource).ConfigureAwait(false); } catch @@ -1313,7 +1313,7 @@ namespace Jellyfin.Api.Controllers } else { - job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType); + job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); if (job?.TranscodingThrottler != null) { await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false); @@ -1330,7 +1330,7 @@ namespace Jellyfin.Api.Controllers } _logger.LogDebug("returning {0} [general case]", segmentPath); - job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType); + job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false); } @@ -1750,7 +1750,7 @@ namespace Jellyfin.Api.Controllers private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension) { - var job = _transcodingJobHelper.GetTranscodingJob(playlist, _transcodingJobType); + var job = _transcodingJobHelper.GetTranscodingJob(playlist, TranscodingJobType); if (job == null || job.HasExited) { diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs index 5e35822bc..13f6fd37c 100644 --- a/Jellyfin.Api/Controllers/VideoHlsController.cs +++ b/Jellyfin.Api/Controllers/VideoHlsController.cs @@ -39,7 +39,7 @@ namespace Jellyfin.Api.Controllers public class VideoHlsController : BaseJellyfinApiController { private const string DefaultEncoderPreset = "superfast"; - private const TranscodingJobType _transcodingJobType = TranscodingJobType.Hls; + private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls; private readonly EncodingHelper _encodingHelper; private readonly IDlnaManager _dlnaManager; @@ -292,7 +292,7 @@ namespace Jellyfin.Api.Controllers _dlnaManager, _deviceManager, _transcodingJobHelper, - _transcodingJobType, + TranscodingJobType, cancellationTokenSource.Token) .ConfigureAwait(false); @@ -315,7 +315,7 @@ namespace Jellyfin.Api.Controllers playlistPath, GetCommandLineArguments(playlistPath, state), Request, - _transcodingJobType, + TranscodingJobType, cancellationTokenSource) .ConfigureAwait(false); job.IsLiveOutput = true; @@ -339,7 +339,7 @@ namespace Jellyfin.Api.Controllers } } - job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType); + job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); if (job != null) { -- cgit v1.2.3