瀏覽代碼

add initial support for HEVC over FMP4-HLS

nyanmisaka 4 年之前
父節點
當前提交
85965741f5

+ 68 - 8
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<string>();
@@ -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;

+ 5 - 2
Jellyfin.Api/Controllers/UniversalAudioController.cs

@@ -191,8 +191,11 @@ namespace Jellyfin.Api.Controllers
             if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
             {
                 // hls segment container can only be mpegts or fmp4 per ffmpeg documentation
+                // ffmpeg option -> file extension
+                //        mpegts -> ts
+                //          fmp4 -> mp4
                 // TODO: remove this when we switch back to the segment muxer
-                var supportedHlsContainers = new[] { "mpegts", "fmp4" };
+                var supportedHlsContainers = new[] { "ts", "mp4" };
 
                 var dynamicHlsRequestDto = new HlsAudioRequestDto
                 {
@@ -201,7 +204,7 @@ namespace Jellyfin.Api.Controllers
                     Static = isStatic,
                     PlaySessionId = info.PlaySessionId,
                     // fallback to mpegts if device reports some weird value unsupported by hls
-                    SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts",
+                    SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "ts",
                     MediaSourceId = mediaSourceId,
                     DeviceId = deviceId,
                     AudioCodec = audioCodec,

+ 179 - 19
Jellyfin.Api/Helpers/DynamicHlsHelper.cs

@@ -202,7 +202,61 @@ namespace Jellyfin.Api.Helpers
                 AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
             }
 
-            AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
+            var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
+
+            if (state.VideoStream != null && state.VideoRequest != null)
+            {
+                // 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)
+                    && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+                {
+                    var requestedVideoProfiles = state.GetRequestedProfiles("hevc");
+                    if (requestedVideoProfiles != null && requestedVideoProfiles.Length > 0)
+                    {
+                        // Force HEVC Main Profile and disable video stream copy
+                        state.OutputVideoCodec = "hevc";
+                        var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(",", requestedVideoProfiles), "main");
+                        sdrVideoUrl += "&AllowVideoStreamCopy=false";
+
+                        EncodingHelper encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
+                        var sdrOutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec) ?? 0;
+                        var sdrOutputAudioBitrate = encodingHelper.GetAudioBitrateParam(state.VideoRequest.AudioBitRate, state.AudioStream) ?? 0;
+                        var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
+
+                        AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
+
+                        // Restore the video codec
+                        state.OutputVideoCodec = "copy";
+                    }
+                }
+
+                // 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)
+                    && state.VideoStream.Level.HasValue
+                    && state.VideoStream.Level > 150
+                    && !string.IsNullOrEmpty(state.VideoStream.VideoRange)
+                    && string.Equals(state.VideoStream.VideoRange, "SDR", StringComparison.OrdinalIgnoreCase)
+                    && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+                {
+                    var playlistCodecsField = new StringBuilder();
+                    AppendPlaylistCodecsField(playlistCodecsField, state);
+
+                    // 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
+                    state.VideoStream.Level = originalLevel;
+                    var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
+                    builder.Append(newPlaylist);
+                }
+            }
 
             if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIp()))
             {
@@ -212,40 +266,77 @@ namespace Jellyfin.Api.Helpers
                 var variation = GetBitrateVariation(totalBitrate);
 
                 var newBitrate = totalBitrate - variation;
-                var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
+                var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
                 AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
 
                 variation *= 2;
                 newBitrate = totalBitrate - variation;
-                variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
+                variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
                 AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
             }
 
             return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
         }
 
-        private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
+        private StringBuilder AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
         {
-            builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
+            var playlistBuilder = new StringBuilder();
+            playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
                 .Append(bitrate.ToString(CultureInfo.InvariantCulture))
                 .Append(",AVERAGE-BANDWIDTH=")
                 .Append(bitrate.ToString(CultureInfo.InvariantCulture));
 
-            AppendPlaylistCodecsField(builder, state);
+            AppendPlaylistVideoRangeField(playlistBuilder, state);
+
+            AppendPlaylistCodecsField(playlistBuilder, state);
 
-            AppendPlaylistResolutionField(builder, state);
+            AppendPlaylistResolutionField(playlistBuilder, state);
 
-            AppendPlaylistFramerateField(builder, state);
+            AppendPlaylistFramerateField(playlistBuilder, state);
 
             if (!string.IsNullOrWhiteSpace(subtitleGroup))
             {
-                builder.Append(",SUBTITLES=\"")
+                playlistBuilder.Append(",SUBTITLES=\"")
                     .Append(subtitleGroup)
                     .Append('"');
             }
 
-            builder.Append(Environment.NewLine);
-            builder.AppendLine(url);
+            playlistBuilder.Append(Environment.NewLine);
+            playlistBuilder.AppendLine(url);
+            builder.Append(playlistBuilder);
+
+            return playlistBuilder;
+        }
+
+        /// <summary>
+        /// Appends a VIDEO-RANGE field containing the range of the output video stream.
+        /// </summary>
+        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+        /// <param name="builder">StringBuilder to append the field to.</param>
+        /// <param name="state">StreamState of the current stream.</param>
+        private void AppendPlaylistVideoRangeField(StringBuilder builder, StreamState state)
+        {
+            if (state.VideoStream != null && !string.IsNullOrEmpty(state.VideoStream.VideoRange))
+            {
+                var videoRange = state.VideoStream.VideoRange;
+                if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+                {
+                    if (string.Equals(videoRange, "SDR", StringComparison.OrdinalIgnoreCase))
+                    {
+                        builder.Append(",VIDEO-RANGE=SDR");
+                    }
+
+                    if (string.Equals(videoRange, "HDR", StringComparison.OrdinalIgnoreCase))
+                    {
+                        builder.Append(",VIDEO-RANGE=PQ");
+                    }
+                }
+                else
+                {
+                    // Currently we only encode to SDR
+                    builder.Append(",VIDEO-RANGE=SDR");
+                }
+            }
         }
 
         /// <summary>
@@ -414,25 +505,68 @@ namespace Jellyfin.Api.Helpers
         /// <returns>H.26X level of the output video stream.</returns>
         private int? GetOutputVideoCodecLevel(StreamState state)
         {
-            string? levelString;
+            string levelString = string.Empty;
             if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+                && state.VideoStream != null
                 && state.VideoStream.Level.HasValue)
             {
-                levelString = state.VideoStream?.Level.ToString();
+                levelString = state.VideoStream.Level.ToString();
             }
             else
             {
-                levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec);
+                if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
+                {
+                    levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec) ?? "41";
+                    levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
+                }
+
+                if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+                {
+                    levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120";
+                    levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
+                }
             }
 
             if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
             {
                 return parsedLevel;
             }
-
             return null;
         }
 
+        /// <summary>
+        /// Get the H.26X profile of the output video stream.
+        /// </summary>
+        /// <param name="state">StreamState of the current stream.</param>
+        /// <param name="codec">Video codec.</param>
+        /// <returns>H.26X profile of the output video stream.</returns>
+        private string GetOutputVideoCodecProfile(StreamState state, string codec)
+        {
+            string profileString = string.Empty;
+            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+                && !string.IsNullOrEmpty(state.VideoStream.Profile))
+            {
+                profileString = state.VideoStream.Profile;
+            }
+            else if (!string.IsNullOrEmpty(codec))
+            {
+                profileString = state.GetRequestedProfiles(codec).FirstOrDefault();
+                if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
+                {
+                    profileString = profileString ?? "high";
+                }
+
+                if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+                {
+                    profileString = profileString ?? "main";
+                }
+            }
+
+            return profileString;
+        }
+
         /// <summary>
         /// Gets a formatted string of the output audio codec, for use in the CODECS field.
         /// </summary>
@@ -463,6 +597,16 @@ namespace Jellyfin.Api.Helpers
                 return HlsCodecStringHelpers.GetEAC3String();
             }
 
+            if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
+            {
+                return HlsCodecStringHelpers.GetFLACString();
+            }
+
+            if (string.Equals(state.ActualOutputAudioCodec, "alac", StringComparison.OrdinalIgnoreCase))
+            {
+                return HlsCodecStringHelpers.GetALACString();
+            }
+
             return string.Empty;
         }
 
@@ -487,15 +631,14 @@ namespace Jellyfin.Api.Helpers
 
             if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
             {
-                string profile = state.GetRequestedProfiles("h264").FirstOrDefault();
+                string profile = GetOutputVideoCodecProfile(state, "h264");
                 return HlsCodecStringHelpers.GetH264String(profile, level);
             }
 
             if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
                 || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
             {
-                string profile = state.GetRequestedProfiles("h265").FirstOrDefault();
-
+                string profile = GetOutputVideoCodecProfile(state, "hevc");
                 return HlsCodecStringHelpers.GetH265String(profile, level);
             }
 
@@ -539,12 +682,29 @@ namespace Jellyfin.Api.Helpers
             return variation;
         }
 
-        private string ReplaceBitrate(string url, int oldValue, int newValue)
+        private string ReplaceVideoBitrate(string url, int oldValue, int newValue)
         {
             return url.Replace(
                 "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
                 "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
                 StringComparison.OrdinalIgnoreCase);
         }
+
+        private string ReplaceProfile(string url, string codec, string oldValue, string newValue)
+        {
+            return url.Replace(
+                codec + "-profile=" + oldValue.ToString(),
+                codec + "-profile=" + newValue.ToString(),
+                StringComparison.OrdinalIgnoreCase);
+        }
+
+        private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
+        {
+            var oldPlaylist = playlist.ToString();
+            return oldPlaylist.Replace(
+                oldValue.ToString(),
+                newValue.ToString(),
+                StringComparison.OrdinalIgnoreCase);
+        }
     }
 }

+ 24 - 5
Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs

@@ -85,20 +85,21 @@ namespace Jellyfin.Api.Helpers
             // The h265 syntax is a bit of a mystery at the time this comment was written.
             // This is what I've found through various sources:
             // FORMAT: [codecTag].[profile].[constraint?].L[level * 30].[UNKNOWN]
-            StringBuilder result = new StringBuilder("hev1", 16);
+            StringBuilder result = new StringBuilder("hvc1", 16);
 
-            if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase))
+            if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase)
+                || string.Equals(profile, "main 10", StringComparison.OrdinalIgnoreCase))
             {
-                result.Append(".2.6");
+                result.Append(".2.4");
             }
             else
             {
                 // Default to main if profile is invalid
-                result.Append(".1.6");
+                result.Append(".1.4");
             }
 
             result.Append(".L")
-                .Append(level * 3)
+                .Append(level)
                 .Append(".B0");
 
             return result.ToString();
@@ -121,5 +122,23 @@ namespace Jellyfin.Api.Helpers
         {
             return "mp4a.a6";
         }
+
+        /// <summary>
+        /// Gets an FLAC codec string.
+        /// </summary>
+        /// <returns>FLAC codec string.</returns>
+        public static string GetFLACString()
+        {
+            return "fLaC";
+        }
+
+        /// <summary>
+        /// Gets an ALAC codec string.
+        /// </summary>
+        /// <returns>ALAC codec string.</returns>
+        public static string GetALACString()
+        {
+            return "alac";
+        }
     }
 }

+ 120 - 67
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -7,6 +7,7 @@ using System.IO;
 using System.Linq;
 using System.Runtime.InteropServices;
 using System.Text;
+using System.Text.RegularExpressions;
 using System.Threading;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities;
@@ -23,7 +24,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 {
     public class EncodingHelper
     {
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IFileSystem _fileSystem;
@@ -654,16 +655,26 @@ namespace MediaBrowser.Controller.MediaEncoding
             return string.Empty;
         }
 
-        public string NormalizeTranscodingLevel(string videoCodec, string level)
+        public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level)
         {
-            // Clients may direct play higher than level 41, but there's no reason to transcode higher
-            if (double.TryParse(level, NumberStyles.Any, _usCulture, out double requestLevel)
-                && requestLevel > 41
-                && (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)
-                    || string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase)
-                    || string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)))
+            if (double.TryParse(level, NumberStyles.Any, _usCulture, out double requestLevel))
             {
-                return "41";
+                if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
+                {
+                    if (requestLevel >= 150)
+                    {
+                        return "150";
+                    }
+                }
+                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
+                    if (requestLevel >= 41)
+                    {
+                        return "41";
+                    }
+                }
             }
 
             return level;
@@ -809,7 +820,8 @@ namespace MediaBrowser.Controller.MediaEncoding
                     param += " -crf " + defaultCrf;
                 }
             }
-            else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)) // h264 (h264_qsv)
+            else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv)
+                || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_qsv)
             {
                 string[] valid_h264_qsv = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" };
 
@@ -825,8 +837,9 @@ namespace MediaBrowser.Controller.MediaEncoding
                 param += " -look_ahead 0";
             }
             else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc)
-                || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase))
+                || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc)
             {
+                // following preset will be deprecated in ffmpeg 4.4, use p1~p7 instead
                 switch (encodingOptions.EncoderPreset)
                 {
                     case "veryslow":
@@ -856,8 +869,8 @@ namespace MediaBrowser.Controller.MediaEncoding
                         break;
                 }
             }
-            else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
-                || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+            else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf)
+                || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_amf)
             {
                 switch (encodingOptions.EncoderPreset)
                 {
@@ -896,6 +909,11 @@ namespace MediaBrowser.Controller.MediaEncoding
                     // Enhance workload when tone mapping with AMF on some APUs
                     param += " -preanalysis true";
                 }
+
+                if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+                {
+                    param += " -header_insertion_mode gop -gops_per_idr 1";
+                }
             }
             else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm
             {
@@ -945,10 +963,24 @@ namespace MediaBrowser.Controller.MediaEncoding
             }
 
             var targetVideoCodec = state.ActualOutputVideoCodec;
+            if (string.Equals(targetVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+                || string.Equals(targetVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+            {
+                targetVideoCodec = "hevc";
+            }
 
             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
+            if (!string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
+                && profile != null
+                && profile.IndexOf("high 10", StringComparison.OrdinalIgnoreCase) != -1)
+            {
+                profile = "high";
+            }
 
-            // vaapi does not support Baseline profile, force Constrained Baseline in this case,
+            // h264_vaapi does not support Baseline profile, force Constrained Baseline in this case,
             // which is compatible (and ugly)
             if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
                 && profile != null
@@ -957,6 +989,24 @@ namespace MediaBrowser.Controller.MediaEncoding
                 profile = "constrained_baseline";
             }
 
+            // 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))
+                    && profile != null
+                    && profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1)
+            {
+                profile = "baseline";
+            }
+
+            // 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)
+            {
+                profile = "main";
+            }
+
             if (!string.IsNullOrEmpty(profile))
             {
                 if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
@@ -971,55 +1021,35 @@ namespace MediaBrowser.Controller.MediaEncoding
 
             if (!string.IsNullOrEmpty(level))
             {
-                level = NormalizeTranscodingLevel(state.OutputVideoCodec, level);
+                level = NormalizeTranscodingLevel(state, level);
 
-                // h264_qsv and h264_nvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format
-                // also needed for libx264 due to https://trac.ffmpeg.org/ticket/3307
+                // 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)
-                    || string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
+                    || string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
                 {
-                    switch (level)
+                    param += " -level " + level;
+                }
+                else if (string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
+                {
+                    // hevc_qsv use -level 51 instead of -level 153
+                    if (double.TryParse(level, NumberStyles.Any, _usCulture, out double hevcLevel))
                     {
-                        case "30":
-                            param += " -level 3.0";
-                            break;
-                        case "31":
-                            param += " -level 3.1";
-                            break;
-                        case "32":
-                            param += " -level 3.2";
-                            break;
-                        case "40":
-                            param += " -level 4.0";
-                            break;
-                        case "41":
-                            param += " -level 4.1";
-                            break;
-                        case "42":
-                            param += " -level 4.2";
-                            break;
-                        case "50":
-                            param += " -level 5.0";
-                            break;
-                        case "51":
-                            param += " -level 5.1";
-                            break;
-                        case "52":
-                            param += " -level 5.2";
-                            break;
-                        default:
-                            param += " -level " + level;
-                            break;
+                        param += " -level " + hevcLevel / 3;
                     }
                 }
+                else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+                {
+                    param += " -level " + level;
+                }
                 else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
                     || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase))
                 {
-                    // nvenc doesn't decode with param -level set ?!
-                    // TODO:
+                    // level option may cause NVENC to fail.
+                    // NVENC cannot adjust the given level, just throw an error.
                 }
-                else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase))
+                else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
+                    || !string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
                 {
                     param += " -level " + level;
                 }
@@ -1032,7 +1062,11 @@ namespace MediaBrowser.Controller.MediaEncoding
 
             if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
             {
-                // todo
+                // 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";
             }
 
             if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
@@ -1040,13 +1074,19 @@ namespace MediaBrowser.Controller.MediaEncoding
                 && !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, "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, "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);
@@ -1708,7 +1748,8 @@ namespace MediaBrowser.Controller.MediaEncoding
                 }
 
                 // For QSV, feed it into hardware encoder now
-                if (isLinux && string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+                if (isLinux && (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)))
                 {
                     videoSizeParam += ",hwupload=extra_hw_frames=64";
                 }
@@ -1729,7 +1770,8 @@ namespace MediaBrowser.Controller.MediaEncoding
                 : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"";
 
             // When the input may or may not be hardware VAAPI decodable
-            if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+            if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
+                || string.Equals(outputVideoCodec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
             {
                 /*
                     [base]: HW scaling video to OutputSize
@@ -1741,7 +1783,8 @@ namespace MediaBrowser.Controller.MediaEncoding
 
             // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
             else if (_mediaEncoder.SupportsHwaccel("vaapi") && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
-                && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
+                && (string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(outputVideoCodec, "libx265", StringComparison.OrdinalIgnoreCase)))
             {
                 /*
                     [base]: SW scaling video to OutputSize
@@ -1750,7 +1793,8 @@ namespace MediaBrowser.Controller.MediaEncoding
                 */
                 retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"";
             }
-            else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+            else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+                || string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
             {
                 /*
                     QSV in FFMpeg can now setup hardware overlay for transcodes.
@@ -1776,7 +1820,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 videoSizeParam);
         }
 
-        private (int? width, int? height) GetFixedOutputSize(
+        public static (int? width, int? height) GetFixedOutputSize(
             int? videoWidth,
             int? videoHeight,
             int? requestedWidth,
@@ -1836,7 +1880,9 @@ namespace MediaBrowser.Controller.MediaEncoding
                 requestedMaxHeight);
 
             if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
-                || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+                || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+                || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
+                || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
                 && width.HasValue
                 && height.HasValue)
             {
@@ -1845,7 +1891,8 @@ namespace MediaBrowser.Controller.MediaEncoding
                 // output dimensions. Output dimensions are guaranteed to be even.
                 var outputWidth = width.Value;
                 var outputHeight = height.Value;
-                var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase);
+                var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase);
                 var isDeintEnabled = state.DeInterlace("h264", true)
                     || state.DeInterlace("avc", true)
                     || state.DeInterlace("h265", true)
@@ -2107,10 +2154,13 @@ namespace MediaBrowser.Controller.MediaEncoding
             var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1;
             var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
             var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
+            var isVaapiHevcEncoder = outputVideoCodec.IndexOf("hevc_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
             var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1;
+            var isQsvHevcEncoder = outputVideoCodec.IndexOf("hevc_qsv", StringComparison.OrdinalIgnoreCase) != -1;
             var isNvdecH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
             var isNvdecHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
             var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1;
+            var isLibX265Encoder = outputVideoCodec.IndexOf("libx265", StringComparison.OrdinalIgnoreCase) != -1;
             var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
             var isColorDepth10 = IsColorDepth10(state);
 
@@ -2185,6 +2235,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                     filters.Add("hwdownload");
 
                     if (isLibX264Encoder
+                        || isLibX265Encoder
                         || hasGraphicalSubs
                         || (isNvdecHevcDecoder && isDeinterlaceHevc)
                         || (!isNvdecHevcDecoder && isDeinterlaceH264 || isDeinterlaceHevc))
@@ -2195,20 +2246,20 @@ namespace MediaBrowser.Controller.MediaEncoding
             }
 
             // When the input may or may not be hardware VAAPI decodable
-            if (isVaapiH264Encoder)
+            if (isVaapiH264Encoder || isVaapiHevcEncoder)
             {
                 filters.Add("format=nv12|vaapi");
                 filters.Add("hwupload");
             }
 
             // When burning in graphical subtitles using overlay_qsv, upload videostream to the same qsv context
-            else if (isLinux && hasGraphicalSubs && isQsvH264Encoder)
+            else if (isLinux && hasGraphicalSubs && (isQsvH264Encoder || isQsvHevcEncoder))
             {
                 filters.Add("hwupload=extra_hw_frames=64");
             }
 
             // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
-            else if (IsVaapiSupported(state) && isVaapiDecoder && isLibX264Encoder)
+            else if (IsVaapiSupported(state) && isVaapiDecoder && (isLibX264Encoder || isLibX265Encoder))
             {
                 var codec = videoStream.Codec.ToLowerInvariant();
 
@@ -2250,7 +2301,9 @@ namespace MediaBrowser.Controller.MediaEncoding
             // Add software deinterlace filter before scaling filter
             if ((isDeinterlaceH264 || isDeinterlaceHevc)
                 && !isVaapiH264Encoder
+                && !isVaapiHevcEncoder
                 && !isQsvH264Encoder
+                && !isQsvHevcEncoder
                 && !isNvdecH264Decoder)
             {
                 if (string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase))
@@ -2289,7 +2342,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             }
 
             // Add parameters to use VAAPI with burn-in text subtitles (GH issue #642)
-            if (isVaapiH264Encoder)
+            if (isVaapiH264Encoder || isVaapiHevcEncoder)
             {
                 if (hasTextSubs)
                 {

+ 0 - 1
MediaBrowser.Model/Dlna/ResolutionNormalizer.cs

@@ -15,7 +15,6 @@ namespace MediaBrowser.Model.Dlna
                 new ResolutionConfiguration(720, 950000),
                 new ResolutionConfiguration(1280, 2500000),
                 new ResolutionConfiguration(1920, 4000000),
-                new ResolutionConfiguration(2560, 8000000),
                 new ResolutionConfiguration(3840, 35000000)
             };