Browse Source

Merge pull request #4432 from nyanmisaka/fmp4-hls

Add initial support for HEVC over FMP4-HLS
Claus Vium 4 years ago
parent
commit
84fe046226

+ 145 - 88
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;
@@ -41,6 +42,9 @@ namespace Jellyfin.Api.Controllers
     [Authorize(Policy = Policies.DefaultAuthorization)]
     public class DynamicHlsController : BaseJellyfinApiController
     {
+        private const string DefaultEncoderPreset = "veryfast";
+        private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
+
         private readonly ILibraryManager _libraryManager;
         private readonly IUserManager _userManager;
         private readonly IDlnaManager _dlnaManager;
@@ -56,8 +60,7 @@ namespace Jellyfin.Api.Controllers
         private readonly ILogger<DynamicHlsController> _logger;
         private readonly EncodingHelper _encodingHelper;
         private readonly DynamicHlsHelper _dynamicHlsHelper;
-
-        private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls;
+        private readonly EncodingOptions _encodingOptions;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="DynamicHlsController"/> class.
@@ -92,6 +95,8 @@ namespace Jellyfin.Api.Controllers
             ILogger<DynamicHlsController> logger,
             DynamicHlsHelper dynamicHlsHelper)
         {
+            _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
+
             _libraryManager = libraryManager;
             _userManager = userManager;
             _dlnaManager = dlnaManager;
@@ -106,8 +111,7 @@ namespace Jellyfin.Api.Controllers
             _transcodingJobHelper = transcodingJobHelper;
             _logger = logger;
             _dynamicHlsHelper = dynamicHlsHelper;
-
-            _encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
+            _encodingOptions = serverConfigurationManager.GetEncodingOptions();
         }
 
         /// <summary>
@@ -272,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);
         }
 
         /// <summary>
@@ -439,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);
         }
 
         /// <summary>
@@ -1129,7 +1133,7 @@ namespace Jellyfin.Api.Controllers
                     _dlnaManager,
                     _deviceManager,
                     _transcodingJobHelper,
-                    _transcodingJobType,
+                    TranscodingJobType,
                     cancellationTokenSource.Token)
                 .ConfigureAwait(false);
 
@@ -1137,11 +1141,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()
@@ -1151,6 +1163,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:")
@@ -1194,7 +1218,7 @@ namespace Jellyfin.Api.Controllers
                     _dlnaManager,
                     _deviceManager,
                     _transcodingJobHelper,
-                    _transcodingJobType,
+                    TranscodingJobType,
                     cancellationTokenSource.Token)
                 .ConfigureAwait(false);
 
@@ -1208,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);
             }
@@ -1222,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);
@@ -1233,7 +1257,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 init file is being requested");
+                        startTranscoding = true;
+                        segmentId = 0;
+                    }
+                    else if (currentTranscodingIndex == null)
                     {
                         _logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
                         startTranscoding = true;
@@ -1265,13 +1295,12 @@ 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,
+                                TranscodingJobType,
                                 cancellationTokenSource).ConfigureAwait(false);
                         }
                         catch
@@ -1284,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);
@@ -1301,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);
         }
 
@@ -1325,11 +1354,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); // GetNumberOfThreads is static.
+            var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
+            var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
 
             if (state.BaseRequest.BreakOnNonKeyFrames)
             {
@@ -1341,36 +1369,57 @@ 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 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 outputTsArg = outputPrefix + "%d" + outputExtension;
 
-            var outputTsArg = Path.Combine(directory, Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request.SegmentContainer);
-
-            var segmentFormat = GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
+            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 + "\"";
+                }
 
-            var maxMuxingQueueSize = encodingOptions.MaxMuxingQueueSize > 128
-                ? encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
+                segmentFormat = "fmp4" + outputFmp4HeaderArg;
+            }
+            else
+            {
+                _logger.LogError("Invalid HLS segment container: " + segmentFormat);
+            }
+
+            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 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 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),
+                _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,
@@ -1379,50 +1428,63 @@ namespace Jellyfin.Api.Controllers
                 outputPath).Trim();
         }
 
-        private string GetAudioArguments(StreamState state, EncodingOptions encodingOptions)
+        /// <summary>
+        /// Gets the audio arguments for transcoding.
+        /// </summary>
+        /// <param name="state">The <see cref="StreamState"/>.</param>
+        /// <returns>The command line arguments for audio transcoding.</returns>
+        private string GetAudioArguments(StreamState state)
         {
+            if (state.AudioStream == null)
+            {
+                return string.Empty;
+            }
+
             var audioCodec = _encodingHelper.GetAudioEncoder(state);
 
             if (!state.IsOutputVideo)
             {
                 if (EncodingHelper.IsCopyCodec(audioCodec))
                 {
-                    return "-acodec copy";
+                    var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
+
+                    return "-acodec copy -strict -2" + bitStreamArgs;
                 }
 
-                var audioTranscodeParams = new List<string>();
+                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 videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
+                var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
 
                 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" + bitStreamArgs;
                 }
 
-                return "-codec:a:0 copy";
+                return "-codec:a:0 copy -strict -2" + bitStreamArgs;
             }
 
             var args = "-codec:a:0 " + audioCodec;
@@ -1446,94 +1508,89 @@ 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)
+        /// <summary>
+        /// Gets the video arguments for transcoding.
+        /// </summary>
+        /// <param name="state">The <see cref="StreamState"/>.</param>
+        /// <param name="startNumber">The first number in the hls sequence.</param>
+        /// <returns>The command line arguments for video transcoding.</returns>
+        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.
+            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
+            // 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))
                 {
-                    string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream);
+                    string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
                     if (!string.IsNullOrEmpty(bitStreamArgs))
                     {
                         args += " " + bitStreamArgs;
                     }
                 }
 
+                args += " -start_at_zero";
+
                 // args += " -flags -global_header";
             }
             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;
+                args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset);
 
-                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));
-                }
+                // Set the key frame params for video encoding to match the hls segment time.
+                args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, false, startNumber);
 
-                args += " " + _encodingHelper.GetVideoQualityParam(state, codec, encodingOptions, "veryfast");
-
-                // Unable to force key frames using these hw 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))
+                // Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
+                if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
                 {
-                    args += " " + gopArg;
-                }
-                else
-                {
-                    args += " " + keyFrameArg + gopArg;
+                    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;
 
-                // 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,
@@ -1693,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)
             {

+ 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,

+ 131 - 40
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;
@@ -296,23 +297,23 @@ namespace Jellyfin.Api.Controllers
                 .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,
                                     cancellationTokenSource)
@@ -328,7 +329,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);
                         }
                     }
                 }
@@ -338,14 +339,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"));
         }
@@ -361,15 +362,44 @@ namespace Jellyfin.Api.Controllers
             var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
             var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec); // GetNumberOfThreads is static.
             var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
-            var format = !string.IsNullOrWhiteSpace(state.Request.SegmentContainer) ? "." + state.Request.SegmentContainer : ".ts";
+            var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
+
             var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
-            var outputTsArg = Path.Combine(directory, Path.GetFileNameWithoutExtension(outputPath)) + "%d" + format;
+            var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
+            var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
+            var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
+            var outputTsArg = outputPrefix + "%d" + outputExtension;
 
-            var segmentFormat = format.TrimStart('.');
+            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}", segmentFormat);
+            }
+
+            var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128
+                ? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
+                : "128";
 
             var baseUrlParam = string.Format(
                 CultureInfo.InvariantCulture,
@@ -378,20 +408,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 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,
-                    _encodingHelper.GetMapArgs(state),
+                    mapArgs,
                     GetVideoArguments(state),
                     GetAudioArguments(state),
+                    maxMuxingQueueSize,
                     state.SegmentLength.ToString(CultureInfo.InvariantCulture),
-                    string.Empty,
                     segmentFormat,
                     baseUrlParam,
-                    outputPath,
-                    outputTsArg)
-                .Trim();
+                    outputTsArg,
+                    outputPath).Trim();
         }
 
         /// <summary>
@@ -401,14 +430,53 @@ namespace Jellyfin.Api.Controllers
         /// <returns>The command line arguments for audio transcoding.</returns>
         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))
+                {
+                    var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
+
+                    return "-acodec copy -strict -2" + bitStreamArgs;
+                }
+
+                var audioTranscodeParams = string.Empty;
+
+                audioTranscodeParams += "-acodec " + audioCodec;
+
+                if (state.OutputAudioBitrate.HasValue)
+                {
+                    audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
+                }
+
+                if (state.OutputAudioChannels.HasValue)
+                {
+                    audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture);
+                }
+
+                if (state.OutputAudioSampleRate.HasValue)
+                {
+                    audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
+                }
+
+                audioTranscodeParams += " -vn";
+                return audioTranscodeParams;
+            }
+
+            if (EncodingHelper.IsCopyCodec(audioCodec))
             {
-                return "-codec:a:0 copy";
+                var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
+
+                return "-acodec copy -strict -2" + bitStreamArgs;
             }
 
-            var args = "-codec:a:0 " + codec;
+            var args = "-codec:a:0 " + audioCodec;
 
             var channels = state.OutputAudioChannels;
 
@@ -429,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;
         }
@@ -441,6 +509,11 @@ namespace Jellyfin.Api.Controllers
         /// <returns>The command line arguments for video transcoding.</returns>
         private string GetVideoArguments(StreamState state)
         {
+            if (state.VideoStream == null)
+            {
+                return string.Empty;
+            }
+
             if (!state.IsOutputVideo)
             {
                 return string.Empty;
@@ -450,47 +523,65 @@ 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);
+                    string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
                     if (!string.IsNullOrEmpty(bitStreamArgs))
                     {
                         args += " " + bitStreamArgs;
                     }
                 }
+
+                args += " -start_at_zero";
             }
             else
             {
-                var keyFrameArg = string.Format(
-                    CultureInfo.InvariantCulture,
-                    " -force_key_frames \"expr:gte(t,n_forced*{0})\"",
-                    state.SegmentLength.ToString(CultureInfo.InvariantCulture));
+                args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset);
 
-                var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+                // Set the key frame params for video encoding to match the hls segment time.
+                args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, true, null);
 
-                args += " " + _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset) + keyFrameArg;
-
-                // Add resolution params, if specified
-                if (!hasGraphicalSubs)
+                // Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
+                if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
                 {
-                    args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec);
+                    args += " -bf 0";
                 }
 
-                // This is for internal graphical subs
+                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";

+ 180 - 18
Jellyfin.Api/Helpers/DynamicHlsHelper.cs

@@ -207,7 +207,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, 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()))
             {
@@ -217,40 +271,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>
@@ -419,15 +510,27 @@ 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() ?? string.Empty;
             }
             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))
@@ -438,6 +541,38 @@ namespace Jellyfin.Api.Helpers
             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() ?? string.Empty;
+                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>
@@ -468,6 +603,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;
         }
 
@@ -492,15 +637,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);
             }
 
@@ -544,12 +688,30 @@ 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)
+        {
+            string profileStr = codec + "-profile=";
+            return url.Replace(
+                profileStr + oldValue,
+                profileStr + newValue,
+                StringComparison.OrdinalIgnoreCase);
+        }
+
+        private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
+        {
+            var oldPlaylist = playlist.ToString();
+            return oldPlaylist.Replace(
+                oldValue.ToString(),
+                newValue.ToString(),
+                StringComparison.OrdinalIgnoreCase);
+        }
     }
 }

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

@@ -9,13 +9,38 @@ namespace Jellyfin.Api.Helpers
     /// </summary>
     public static class HlsCodecStringHelpers
     {
+        /// <summary>
+        /// Codec name for MP3.
+        /// </summary>
+        public const string MP3 = "mp4a.40.34";
+
+        /// <summary>
+        /// Codec name for AC-3.
+        /// </summary>
+        public const string AC3 = "mp4a.a5";
+
+        /// <summary>
+        /// Codec name for E-AC-3.
+        /// </summary>
+        public const string EAC3 = "mp4a.a6";
+
+        /// <summary>
+        /// Codec name for FLAC.
+        /// </summary>
+        public const string FLAC = "fLaC";
+
+        /// <summary>
+        /// Codec name for ALAC.
+        /// </summary>
+        public const string ALAC = "alac";
+
         /// <summary>
         /// Gets a MP3 codec string.
         /// </summary>
         /// <returns>MP3 codec string.</returns>
         public static string GetMP3String()
         {
-            return "mp4a.40.34";
+            return MP3;
         }
 
         /// <summary>
@@ -40,6 +65,42 @@ namespace Jellyfin.Api.Helpers
             return result.ToString();
         }
 
+        /// <summary>
+        /// Gets an AC-3 codec string.
+        /// </summary>
+        /// <returns>AC-3 codec string.</returns>
+        public static string GetAC3String()
+        {
+            return AC3;
+        }
+
+        /// <summary>
+        /// Gets an E-AC-3 codec string.
+        /// </summary>
+        /// <returns>E-AC-3 codec string.</returns>
+        public static string GetEAC3String()
+        {
+            return EAC3;
+        }
+
+        /// <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;
+        }
+
         /// <summary>
         /// Gets a H.264 codec string.
         /// </summary>
@@ -85,41 +146,24 @@ 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();
         }
-
-        /// <summary>
-        /// Gets an AC-3 codec string.
-        /// </summary>
-        /// <returns>AC-3 codec string.</returns>
-        public static string GetAC3String()
-        {
-            return "mp4a.a5";
-        }
-
-        /// <summary>
-        /// Gets an E-AC-3 codec string.
-        /// </summary>
-        /// <returns>E-AC-3 codec string.</returns>
-        public static string GetEAC3String()
-        {
-            return "mp4a.a6";
-        }
     }
 }

+ 50 - 7
Jellyfin.Api/Helpers/HlsHelpers.cs

@@ -1,8 +1,11 @@
 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.Controller.MediaEncoding;
 using MediaBrowser.Model.IO;
 using Microsoft.Extensions.Logging;
 
@@ -74,25 +77,65 @@ namespace Jellyfin.Api.Helpers
             }
         }
 
+        /// <summary>
+        /// Gets the #EXT-X-MAP string.
+        /// </summary>
+        /// <param name="outputPath">The output path of the file.</param>
+        /// <param name="state">The <see cref="StreamState"/>.</param>
+        /// <param name="isOsDepends">Get a normal string or depends on OS.</param>
+        /// <returns>The string text of #EXT-X-MAP.</returns>
+        public static string GetFmp4InitFileName(string outputPath, StreamState state, bool isOsDepends)
+        {
+            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 = EncodingHelper.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;
+        }
+
         /// <summary>
         /// Gets the hls playlist text.
         /// </summary>
         /// <param name="path">The path to the playlist file.</param>
-        /// <param name="segmentLength">The segment length.</param>
+        /// <param name="state">The <see cref="StreamState"/>.</param>
         /// <returns>The playlist text as a string.</returns>
-        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 = EncodingHelper.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;
         }

+ 38 - 15
Jellyfin.Api/Helpers/StreamingHelpers.cs

@@ -169,7 +169,9 @@ namespace Jellyfin.Api.Helpers
                 state.DirectStreamProvider = liveStreamInfo.Item2;
             }
 
-            encodingHelper.AttachMediaSourceInfo(state, mediaSource, url);
+            var encodingOptions = serverConfigurationManager.GetEncodingOptions();
+
+            encodingHelper.AttachMediaSourceInfo(state, encodingOptions, mediaSource, url);
 
             string? containerInternal = Path.GetExtension(state.RequestedUrl);
 
@@ -187,7 +189,7 @@ namespace Jellyfin.Api.Helpers
 
             state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
 
-            state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, state.AudioStream);
+            state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream);
 
             state.OutputAudioCodec = streamingRequest.AudioCodec;
 
@@ -200,20 +202,41 @@ namespace Jellyfin.Api.Helpers
 
                 encodingHelper.TryStreamCopy(state);
 
-                if (state.OutputVideoBitrate.HasValue && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+                if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue)
                 {
-                    var resolution = ResolutionNormalizer.Normalize(
-                        state.VideoStream?.BitRate,
-                        state.VideoStream?.Width,
-                        state.VideoStream?.Height,
-                        state.OutputVideoBitrate.Value,
-                        state.VideoStream?.Codec,
-                        state.OutputVideoCodec,
-                        state.VideoRequest.MaxWidth,
-                        state.VideoRequest.MaxHeight);
-
-                    state.VideoRequest.MaxWidth = resolution.MaxWidth;
-                    state.VideoRequest.MaxHeight = resolution.MaxHeight;
+                    var isVideoResolutionNotRequested = !state.VideoRequest.Width.HasValue
+                        && !state.VideoRequest.Height.HasValue
+                        && !state.VideoRequest.MaxWidth.HasValue
+                        && !state.VideoRequest.MaxHeight.HasValue;
+
+                    if (isVideoResolutionNotRequested
+                        && state.VideoRequest.VideoBitRate.HasValue
+                        && state.VideoStream.BitRate.HasValue
+                        && state.VideoRequest.VideoBitRate.Value >= state.VideoStream.BitRate.Value)
+                    {
+                        // Don't downscale the resolution if the width/height/MaxWidth/MaxHeight is not requested,
+                        // and the requested video bitrate is higher than source video bitrate.
+                        if (state.VideoStream.Width.HasValue || state.VideoStream.Height.HasValue)
+                        {
+                            state.VideoRequest.MaxWidth = state.VideoStream?.Width;
+                            state.VideoRequest.MaxHeight = state.VideoStream?.Height;
+                        }
+                    }
+                    else
+                    {
+                        var resolution = ResolutionNormalizer.Normalize(
+                            state.VideoStream?.BitRate,
+                            state.VideoStream?.Width,
+                            state.VideoStream?.Height,
+                            state.OutputVideoBitrate.Value,
+                            state.VideoStream?.Codec,
+                            state.OutputVideoCodec,
+                            state.VideoRequest.MaxWidth,
+                            state.VideoRequest.MaxHeight);
+
+                        state.VideoRequest.MaxWidth = resolution.MaxWidth;
+                        state.VideoRequest.MaxHeight = resolution.MaxHeight;
+                    }
                 }
             }
 

+ 2 - 1
Jellyfin.Api/Helpers/TranscodingJobHelper.cs

@@ -771,8 +771,9 @@ namespace Jellyfin.Api.Helpers
                     new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
                     cancellationTokenSource.Token)
                     .ConfigureAwait(false);
+                var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
 
-                _encodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.RequestedUrl);
+                _encodingHelper.AttachMediaSourceInfo(state, encodingOptions, liveStreamResponse.MediaSource, state.RequestedUrl);
 
                 if (state.VideoRequest != null)
                 {

+ 378 - 147
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;
@@ -440,6 +441,12 @@ namespace MediaBrowser.Controller.MediaEncoding
                 return "libopus";
             }
 
+            if (string.Equals(codec, "flac", StringComparison.OrdinalIgnoreCase))
+            {
+                // flac is experimental in mp4 muxer
+                return "flac -strict -2";
+            }
+
             return codec.ToLowerInvariant();
         }
 
@@ -573,7 +580,7 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// </summary>
         /// <param name="stream">The stream.</param>
         /// <returns><c>true</c> if the specified stream is H264; otherwise, <c>false</c>.</returns>
-        public bool IsH264(MediaStream stream)
+        public static bool IsH264(MediaStream stream)
         {
             var codec = stream.Codec ?? string.Empty;
 
@@ -581,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;
 
@@ -589,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 string GetBitStreamArgs(MediaStream stream)
+        public static bool IsAAC(MediaStream stream)
         {
+            var codec = stream.Codec ?? string.Empty;
+
+            return codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1;
+        }
+
+        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
             if (IsH264(stream))
             {
                 return "-bsf:v h264_mp4toannexb";
@@ -601,12 +615,44 @@ 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;
             }
         }
 
+        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;
@@ -654,16 +700,30 @@ 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))
+                {
+                    // 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";
+                    }
+                }
+                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;
@@ -766,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;
+        }
+
         /// <summary>
         /// Gets the video bitrate to specify on the command line.
         /// </summary>
@@ -773,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);
@@ -781,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;
@@ -809,38 +976,40 @@ 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" };
 
                 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";
             }
             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":
 
-                        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":
@@ -848,27 +1017,27 @@ 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;
                 }
             }
-            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)
                 {
                     case "veryslow":
                     case "slow":
                     case "slower":
-                        param += "-quality quality";
+                        param += " -quality quality";
                         break;
 
                     case "medium":
-                        param += "-quality balanced";
+                        param += " -quality balanced";
                         break;
 
                     case "fast":
@@ -876,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;
                 }
 
@@ -896,6 +1065,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
             {
@@ -917,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,
@@ -925,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);
@@ -945,11 +1119,25 @@ 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);
 
-            // vaapi does not support Baseline profile, force Constrained Baseline in this case,
-            // which is compatible (and ugly)
+            // 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";
+            }
+
+            // 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
                 && profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1)
@@ -957,13 +1145,31 @@ 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)
                     && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
                 {
                     // not supported by h264_omx
-                    param += " -profile:v " + profile;
+                    param += " -profile:v:0 " + profile;
                 }
             }
 
@@ -971,55 +1177,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))
+                         || 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,42 +1218,11 @@ namespace MediaBrowser.Controller.MediaEncoding
 
             if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
             {
-                // todo
-            }
-
-            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))
-            {
-                param = "-pix_fmt yuv420p " + param;
-            }
-
-            if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
-                || string.Equals(videoEncoder, "h264_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;
+                // 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";
             }
 
             return param;
@@ -1346,7 +1501,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
                 || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
             {
-                return .5;
+                return .6;
             }
 
             return 1;
@@ -1380,36 +1535,48 @@ namespace MediaBrowser.Controller.MediaEncoding
 
         public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream)
         {
-            if (audioStream == null)
-            {
-                return null;
-            }
-
-            if (request.AudioBitRate.HasValue)
-            {
-                // Don't encode any higher than this
-                return Math.Min(384000, request.AudioBitRate.Value);
-            }
-
-            // Empty bitrate area is not allow on iOS
-            // Default audio bitrate to 128K if it is not being requested
-            // https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
-            return 128000;
+            return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream);
         }
 
-        public int? GetAudioBitrateParam(int? audioBitRate, MediaStream audioStream)
+        public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream)
         {
             if (audioStream == null)
             {
                 return null;
             }
 
-            if (audioBitRate.HasValue)
+            if (audioBitRate.HasValue && string.IsNullOrEmpty(audioCodec))
             {
-                // Don't encode any higher than this
                 return Math.Min(384000, audioBitRate.Value);
             }
 
+            if (audioBitRate.HasValue && !string.IsNullOrEmpty(audioCodec))
+            {
+                if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
+                {
+                    if ((audioStream.Channels ?? 0) >= 6)
+                    {
+                        return Math.Min(640000, audioBitRate.Value);
+                    }
+
+                    return Math.Min(384000, audioBitRate.Value);
+                }
+
+                if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase))
+                {
+                    if ((audioStream.Channels ?? 0) >= 6)
+                    {
+                        return Math.Min(3584000, audioBitRate.Value);
+                    }
+
+                    return Math.Min(1536000, audioBitRate.Value);
+                }
+            }
+
             // Empty bitrate area is not allow on iOS
             // Default audio bitrate to 128K if it is not being requested
             // https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
@@ -1447,7 +1614,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 
             if (filters.Count > 0)
             {
-                return "-af \"" + string.Join(",", filters) + "\"";
+                return " -af \"" + string.Join(",", filters) + "\"";
             }
 
             return string.Empty;
@@ -1462,6 +1629,11 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <returns>System.Nullable{System.Int32}.</returns>
         public int? GetNumAudioChannelsParam(EncodingJobInfo state, MediaStream audioStream, string outputAudioCodec)
         {
+            if (audioStream == null)
+            {
+                return null;
+            }
+
             var request = state.BaseRequest;
 
             var inputChannels = audioStream?.Channels;
@@ -1484,6 +1656,11 @@ namespace MediaBrowser.Controller.MediaEncoding
                 // libmp3lame currently only supports two channel output
                 transcoderChannelLimit = 2;
             }
+            else if (codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1)
+            {
+                // aac is able to handle 8ch(7.1 layout)
+                transcoderChannelLimit = 8;
+            }
             else
             {
                 // If we don't have any media info then limit it to 6 to prevent encoding errors due to asking for too many channels
@@ -1708,7 +1885,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 +1907,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 +1920,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 +1930,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 +1957,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 +2017,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 +2028,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 +2291,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 +2372,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                     filters.Add("hwdownload");
 
                     if (isLibX264Encoder
+                        || isLibX265Encoder
                         || hasGraphicalSubs
                         || (isNvdecHevcDecoder && isDeinterlaceHevc)
                         || (!isNvdecHevcDecoder && isDeinterlaceH264 || isDeinterlaceHevc))
@@ -2195,20 +2383,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 +2438,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 +2479,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)
                 {
@@ -2562,6 +2752,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 
         public void AttachMediaSourceInfo(
             EncodingJobInfo state,
+            EncodingOptions encodingOptions,
             MediaSourceInfo mediaSource,
             string requestedUrl)
         {
@@ -2692,11 +2883,23 @@ namespace MediaBrowser.Controller.MediaEncoding
                 request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => _mediaEncoder.CanEncodeToAudioCodec(i))
                     ?? state.SupportedAudioCodecs.FirstOrDefault();
             }
+
+            var supportedVideoCodecs = state.SupportedVideoCodecs;
+            if (request != null && supportedVideoCodecs != null && supportedVideoCodecs.Length > 0)
+            {
+                var supportedVideoCodecsList = supportedVideoCodecs.ToList();
+
+                ShiftVideoCodecsIfNeeded(supportedVideoCodecsList, encodingOptions);
+
+                state.SupportedVideoCodecs = supportedVideoCodecsList.ToArray();
+
+                request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
+            }
         }
 
         private void ShiftAudioCodecsIfNeeded(List<string> audioCodecs, MediaStream audioStream)
         {
-            // Nothing to do here
+            // No need to shift if there is only one supported audio codec.
             if (audioCodecs.Count < 2)
             {
                 return;
@@ -2724,6 +2927,34 @@ namespace MediaBrowser.Controller.MediaEncoding
             }
         }
 
+        private void ShiftVideoCodecsIfNeeded(List<string> videoCodecs, EncodingOptions encodingOptions)
+        {
+            // Shift hevc/h265 to the end of list if hevc encoding is not allowed.
+            if (encodingOptions.AllowHevcEncoding)
+            {
+                return;
+            }
+
+            // No need to shift if there is only one supported video codec.
+            if (videoCodecs.Count < 2)
+            {
+                return;
+            }
+
+            var shiftVideoCodecs = new[] { "hevc", "h265" };
+            if (videoCodecs.All(i => shiftVideoCodecs.Contains(i, StringComparer.OrdinalIgnoreCase)))
+            {
+                return;
+            }
+
+            while (shiftVideoCodecs.Contains(videoCodecs[0], StringComparer.OrdinalIgnoreCase))
+            {
+                var removed = shiftVideoCodecs[0];
+                videoCodecs.RemoveAt(0);
+                videoCodecs.Add(removed);
+            }
+        }
+
         private void NormalizeSubtitleEmbed(EncodingJobInfo state)
         {
             if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed)
@@ -3357,7 +3588,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 args += " -ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture);
             }
 
-            args += " " + GetAudioFilterParam(state, encodingOptions, false);
+            args += GetAudioFilterParam(state, encodingOptions, false);
 
             return args;
         }

+ 10 - 0
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;

+ 103 - 2
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -234,8 +234,8 @@ namespace MediaBrowser.MediaEncoding.Probing
 
             var channelsValue = channels.Value;
 
-            if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
+            if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase)
+                || string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
             {
                 if (channelsValue <= 2)
                 {
@@ -248,6 +248,34 @@ namespace MediaBrowser.MediaEncoding.Probing
                 }
             }
 
+            if (string.Equals(codec, "ac3", StringComparison.OrdinalIgnoreCase)
+                || string.Equals(codec, "eac3", StringComparison.OrdinalIgnoreCase))
+            {
+                if (channelsValue <= 2)
+                {
+                    return 192000;
+                }
+
+                if (channelsValue >= 5)
+                {
+                    return 640000;
+                }
+            }
+
+            if (string.Equals(codec, "flac", StringComparison.OrdinalIgnoreCase)
+                || string.Equals(codec, "alac", StringComparison.OrdinalIgnoreCase))
+            {
+                if (channelsValue <= 2)
+                {
+                    return 960000;
+                }
+
+                if (channelsValue >= 5)
+                {
+                    return 2880000;
+                }
+            }
+
             return null;
         }
 
@@ -774,6 +802,35 @@ namespace MediaBrowser.MediaEncoding.Probing
                 stream.BitRate = bitrate;
             }
 
+            // Extract bitrate info from tag "BPS" if possible.
+            if (!stream.BitRate.HasValue
+                && (string.Equals(streamInfo.CodecType, "audio", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase)))
+            {
+                var bps = GetBPSFromTags(streamInfo);
+                if (bps != null && bps > 0)
+                {
+                    stream.BitRate = bps;
+                }
+            }
+
+            // Get average bitrate info from tag "NUMBER_OF_BYTES" and "DURATION" if possible.
+            if (!stream.BitRate.HasValue
+                && (string.Equals(streamInfo.CodecType, "audio", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase)))
+            {
+                var durationInSeconds = GetRuntimeSecondsFromTags(streamInfo);
+                var bytes = GetNumberOfBytesFromTags(streamInfo);
+                if (durationInSeconds != null && bytes != null)
+                {
+                    var bps = Convert.ToInt32(bytes * 8 / durationInSeconds, CultureInfo.InvariantCulture);
+                    if (bps > 0)
+                    {
+                        stream.BitRate = bps;
+                    }
+                }
+            }
+
             var disposition = streamInfo.Disposition;
             if (disposition != null)
             {
@@ -963,6 +1020,50 @@ namespace MediaBrowser.MediaEncoding.Probing
             }
         }
 
+        private int? GetBPSFromTags(MediaStreamInfo streamInfo)
+        {
+            if (streamInfo != null && streamInfo.Tags != null)
+            {
+                var bps = GetDictionaryValue(streamInfo.Tags, "BPS-eng") ?? GetDictionaryValue(streamInfo.Tags, "BPS");
+                if (!string.IsNullOrEmpty(bps)
+                    && int.TryParse(bps, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBps))
+                {
+                    return parsedBps;
+                }
+            }
+
+            return null;
+        }
+
+        private double? GetRuntimeSecondsFromTags(MediaStreamInfo streamInfo)
+        {
+            if (streamInfo != null && streamInfo.Tags != null)
+            {
+                var duration = GetDictionaryValue(streamInfo.Tags, "DURATION-eng") ?? GetDictionaryValue(streamInfo.Tags, "DURATION");
+                if (!string.IsNullOrEmpty(duration) && TimeSpan.TryParse(duration, out var parsedDuration))
+                {
+                    return parsedDuration.TotalSeconds;
+                }
+            }
+
+            return null;
+        }
+
+        private long? GetNumberOfBytesFromTags(MediaStreamInfo streamInfo)
+        {
+            if (streamInfo != null && streamInfo.Tags != null)
+            {
+                var numberOfBytes = GetDictionaryValue(streamInfo.Tags, "NUMBER_OF_BYTES-eng") ?? GetDictionaryValue(streamInfo.Tags, "NUMBER_OF_BYTES");
+                if (!string.IsNullOrEmpty(numberOfBytes)
+                    && long.TryParse(numberOfBytes, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBytes))
+                {
+                    return parsedBytes;
+                }
+            }
+
+            return null;
+        }
+
         private void SetSize(InternalMediaInfoResult data, MediaInfo info)
         {
             if (data.Format != null)

+ 3 - 0
MediaBrowser.Model/Configuration/EncodingOptions.cs

@@ -67,6 +67,8 @@ namespace MediaBrowser.Model.Configuration
 
         public bool EnableHardwareEncoding { get; set; }
 
+        public bool AllowHevcEncoding { get; set; }
+
         public bool EnableSubtitleExtraction { get; set; }
 
         public string[] HardwareDecodingCodecs { get; set; }
@@ -99,6 +101,7 @@ namespace MediaBrowser.Model.Configuration
             EnableDecodingColorDepth10Hevc = true;
             EnableDecodingColorDepth10Vp9 = true;
             EnableHardwareEncoding = true;
+            AllowHevcEncoding = true;
             EnableSubtitleExtraction = true;
             HardwareDecodingCodecs = new string[] { "h264", "vc1" };
         }

+ 5 - 5
MediaBrowser.Model/Dlna/ResolutionNormalizer.cs

@@ -15,7 +15,7 @@ namespace MediaBrowser.Model.Dlna
                 new ResolutionConfiguration(720, 950000),
                 new ResolutionConfiguration(1280, 2500000),
                 new ResolutionConfiguration(1920, 4000000),
-                new ResolutionConfiguration(2560, 8000000),
+                new ResolutionConfiguration(2560, 20000000),
                 new ResolutionConfiguration(3840, 35000000)
             };
 
@@ -80,11 +80,11 @@ namespace MediaBrowser.Model.Dlna
 
         private static double GetVideoBitrateScaleFactor(string codec)
         {
-            if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
+            if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
+                || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
+                || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
             {
-                return .5;
+                return .6;
             }
 
             return 1;

+ 64 - 8
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -872,11 +872,34 @@ namespace MediaBrowser.Model.Dlna
             return playlistItem;
         }
 
-        private static int GetDefaultAudioBitrateIfUnknown(MediaStream audioStream)
+        private static int GetDefaultAudioBitrate(string audioCodec, int? audioChannels)
         {
-            if ((audioStream.Channels ?? 0) >= 6)
+            if (!string.IsNullOrEmpty(audioCodec))
             {
-                return 384000;
+                // Default to a higher bitrate for stream copy
+                if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
+                {
+                    if ((audioChannels ?? 0) < 2)
+                    {
+                        return 128000;
+                    }
+
+                    return (audioChannels ?? 0) >= 6 ? 640000 : 384000;
+                }
+
+                if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase))
+                {
+                    if ((audioChannels ?? 0) < 2)
+                    {
+                        return 768000;
+                    }
+
+                    return (audioChannels ?? 0) >= 6 ? 3584000 : 1536000;
+                }
             }
 
             return 192000;
@@ -897,14 +920,27 @@ namespace MediaBrowser.Model.Dlna
             }
             else
             {
-                if (targetAudioChannels.HasValue && audioStream.Channels.HasValue && targetAudioChannels.Value < audioStream.Channels.Value)
+                if (targetAudioChannels.HasValue
+                    && audioStream.Channels.HasValue
+                    && audioStream.Channels.Value > targetAudioChannels.Value)
                 {
-                    // Reduce the bitrate if we're downmixing
-                    defaultBitrate = targetAudioChannels.Value < 2 ? 128000 : 192000;
+                    // Reduce the bitrate if we're downmixing.
+                    defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, targetAudioChannels);
+                }
+                else if (targetAudioChannels.HasValue
+                         && audioStream.Channels.HasValue
+                         && audioStream.Channels.Value <= targetAudioChannels.Value
+                         && !string.IsNullOrEmpty(audioStream.Codec)
+                         && targetAudioCodecs != null
+                         && targetAudioCodecs.Length > 0
+                         && !Array.Exists(targetAudioCodecs, elem => string.Equals(audioStream.Codec, elem, StringComparison.OrdinalIgnoreCase)))
+                {
+                    // Shift the bitrate if we're transcoding to a different audio codec.
+                    defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, audioStream.Channels.Value);
                 }
                 else
                 {
-                    defaultBitrate = audioStream.BitRate ?? GetDefaultAudioBitrateIfUnknown(audioStream);
+                    defaultBitrate = audioStream.BitRate ?? GetDefaultAudioBitrate(targetAudioCodec, targetAudioChannels);
                 }
 
                 // Seeing webm encoding failures when source has 1 audio channel and 22k bitrate.
@@ -938,8 +974,28 @@ namespace MediaBrowser.Model.Dlna
             {
                 return 448000;
             }
+            else if (totalBitrate <= 4000000)
+            {
+                return 640000;
+            }
+            else if (totalBitrate <= 5000000)
+            {
+                return 768000;
+            }
+            else if (totalBitrate <= 10000000)
+            {
+                return 1536000;
+            }
+            else if (totalBitrate <= 15000000)
+            {
+                return 2304000;
+            }
+            else if (totalBitrate <= 20000000)
+            {
+                return 3584000;
+            }
 
-            return 640000;
+            return 7168000;
         }
 
         private (PlayMethod?, List<TranscodeReason>) GetVideoDirectPlayProfile(

+ 1 - 1
MediaBrowser.Model/Dlna/StreamInfo.cs

@@ -794,7 +794,7 @@ namespace MediaBrowser.Model.Dlna
 
         public int? GetTargetAudioChannels(string codec)
         {
-            var defaultValue = GlobalMaxAudioChannels;
+            var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels;
 
             var value = GetOption(codec, "audiochannels");
             if (string.IsNullOrEmpty(value))