aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs43
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs13
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs16
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs168
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs5
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs6
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs24
7 files changed, 211 insertions, 64 deletions
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 16c77a923..b827daa3a 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -12,6 +12,7 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Extensions;
using Jellyfin.MediaEncoding.Hls.Playlist;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
@@ -1687,14 +1688,25 @@ public class DynamicHlsController : BaseJellyfinApiController
audioTranscodeParams += "-acodec " + audioCodec;
- if (state.OutputAudioBitrate.HasValue)
+ var audioBitrate = state.OutputAudioBitrate;
+ var audioChannels = state.OutputAudioChannels;
+
+ if (audioBitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(state.ActualOutputAudioCodec, StringComparison.OrdinalIgnoreCase))
{
- audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
+ var vbrParam = _encodingHelper.GetAudioVbrModeParam(state.OutputAudioCodec, audioBitrate.Value / (audioChannels ?? 2));
+ if (_encodingOptions.EnableAudioVbr && vbrParam is not null)
+ {
+ audioTranscodeParams += vbrParam;
+ }
+ else
+ {
+ audioTranscodeParams += " -ab " + audioBitrate.Value.ToString(CultureInfo.InvariantCulture);
+ }
}
- if (state.OutputAudioChannels.HasValue)
+ if (audioChannels.HasValue)
{
- audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture);
+ audioTranscodeParams += " -ac " + audioChannels.Value.ToString(CultureInfo.InvariantCulture);
}
if (state.OutputAudioSampleRate.HasValue)
@@ -1708,11 +1720,11 @@ public class DynamicHlsController : BaseJellyfinApiController
// dts, flac, opus and truehd are experimental in mp4 muxer
var strictArgs = string.Empty;
-
- if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
+ var actualOutputAudioCodec = state.ActualOutputAudioCodec;
+ if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
{
strictArgs = " -strict -2";
}
@@ -1746,10 +1758,17 @@ public class DynamicHlsController : BaseJellyfinApiController
}
var bitrate = state.OutputAudioBitrate;
-
- if (bitrate.HasValue)
+ if (bitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(actualOutputAudioCodec, StringComparison.OrdinalIgnoreCase))
{
- args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
+ var vbrParam = _encodingHelper.GetAudioVbrModeParam(actualOutputAudioCodec, bitrate.Value / (channels ?? 2));
+ if (_encodingOptions.EnableAudioVbr && vbrParam is not null)
+ {
+ args += vbrParam;
+ }
+ else
+ {
+ args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
+ }
}
if (state.OutputAudioSampleRate.HasValue)
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index 245239233..4486954c6 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
@@ -223,9 +224,17 @@ public class DynamicHlsHelper
sdrVideoUrl += "&AllowVideoStreamCopy=false";
var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
- var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0;
- var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
+ var sdrOutputAudioBitrate = 0;
+ if (EncodingHelper.LosslessAudioCodecs.Contains(state.VideoRequest.AudioCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ sdrOutputAudioBitrate = state.AudioStream.BitRate ?? 0;
+ }
+ else
+ {
+ sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream, state.OutputAudioChannels) ?? 0;
+ }
+ var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
// Provide a workaround for the case issue between flac and fLaC.
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 9b5a14c4d..9c91dcc6f 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -181,12 +181,18 @@ public static class StreamingHelpers
: GetOutputFileExtension(state, mediaSource);
}
- state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
-
- state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream);
-
- state.OutputAudioCodec = streamingRequest.AudioCodec;
+ var outputAudioCodec = streamingRequest.AudioCodec;
+ if (EncodingHelper.LosslessAudioCodecs.Contains(outputAudioCodec))
+ {
+ state.OutputAudioBitrate = state.AudioStream.BitRate ?? 0;
+ }
+ else
+ {
+ state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0;
+ }
+ state.OutputAudioCodec = outputAudioCodec;
+ state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
if (state.VideoRequest is not null)
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 5de57917e..960b0b7cd 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -89,6 +89,16 @@ namespace MediaBrowser.Controller.MediaEncoding
{ "truehd", 6 },
};
+ public static readonly string[] LosslessAudioCodecs = new string[]
+ {
+ "alac",
+ "ape",
+ "flac",
+ "mlp",
+ "truehd",
+ "wavpack"
+ };
+
public EncodingHelper(
IApplicationPaths appPaths,
IMediaEncoder mediaEncoder,
@@ -620,6 +630,11 @@ namespace MediaBrowser.Controller.MediaEncoding
return "flac";
}
+ if (string.Equals(codec, "dts", StringComparison.OrdinalIgnoreCase))
+ {
+ return "dca";
+ }
+
return codec.ToLowerInvariant();
}
@@ -2050,9 +2065,9 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
- // Video bitrate must fall within requested value
+ // Audio bitrate must fall within requested value
if (request.AudioBitRate.HasValue
- && audioStream.BitDepth.HasValue
+ && audioStream.BitRate.HasValue
&& audioStream.BitRate.Value > request.AudioBitRate.Value)
{
return false;
@@ -2161,56 +2176,96 @@ namespace MediaBrowser.Controller.MediaEncoding
return Convert.ToInt32(scaleFactor * bitrate);
}
- public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream)
+ public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream, int? outputAudioChannels)
{
- return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream);
+ return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream, outputAudioChannels);
}
- public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream)
+ public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream, int? outputAudioChannels)
{
if (audioStream is null)
{
return null;
}
- if (audioBitRate.HasValue && string.IsNullOrEmpty(audioCodec))
+ var inputChannels = audioStream.Channels ?? 0;
+ var outputChannels = outputAudioChannels ?? 0;
+ var bitrate = audioBitRate ?? int.MaxValue;
+
+ if (string.IsNullOrEmpty(audioCodec)
+ || string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "opus", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "vorbis", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
{
- return Math.Min(384000, audioBitRate.Value);
+ return (inputChannels, outputChannels) switch
+ {
+ (>= 6, >= 6 or 0) => Math.Min(640000, bitrate),
+ (> 0, > 0) => Math.Min(outputChannels * 128000, bitrate),
+ (> 0, _) => Math.Min(inputChannels * 128000, bitrate),
+ (_, _) => Math.Min(384000, bitrate)
+ };
}
- if (audioBitRate.HasValue && !string.IsNullOrEmpty(audioCodec))
+ if (string.Equals(audioCodec, "dts", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "dca", StringComparison.OrdinalIgnoreCase))
{
- if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
- || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
- || string.Equals(audioCodec, "opus", StringComparison.OrdinalIgnoreCase)
- || string.Equals(audioCodec, "vorbis", StringComparison.OrdinalIgnoreCase)
- || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
- || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
+ return (inputChannels, outputChannels) switch
{
- if ((audioStream.Channels ?? 0) >= 6)
- {
- return Math.Min(640000, audioBitRate.Value);
- }
+ (>= 6, >= 6 or 0) => Math.Min(768000, bitrate),
+ (> 0, > 0) => Math.Min(outputChannels * 136000, bitrate),
+ (> 0, _) => Math.Min(inputChannels * 136000, bitrate),
+ (_, _) => Math.Min(672000, bitrate)
+ };
+ }
- return Math.Min(384000, audioBitRate.Value);
- }
+ // Empty bitrate area is not allow on iOS
+ // Default audio bitrate to 128K per channel if we don't have codec specific defaults
+ // https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
+ return 128000 * (outputAudioChannels ?? audioStream.Channels ?? 2);
+ }
- if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase)
- || string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase))
+ public string GetAudioVbrModeParam(string encoder, int bitratePerChannel)
+ {
+ if (string.Equals(encoder, "libfdk_aac", StringComparison.OrdinalIgnoreCase))
+ {
+ return " -vbr:a " + bitratePerChannel switch
{
- if ((audioStream.Channels ?? 0) >= 6)
- {
- return Math.Min(3584000, audioBitRate.Value);
- }
+ < 32000 => "1",
+ < 48000 => "2",
+ < 64000 => "3",
+ < 96000 => "4",
+ _ => "5"
+ };
+ }
- return Math.Min(1536000, audioBitRate.Value);
- }
+ if (string.Equals(encoder, "libmp3lame", StringComparison.OrdinalIgnoreCase))
+ {
+ return " -qscale:a " + bitratePerChannel switch
+ {
+ < 48000 => "8",
+ < 64000 => "6",
+ < 88000 => "4",
+ < 112000 => "2",
+ _ => "0"
+ };
}
- // 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;
+ if (string.Equals(encoder, "libvorbis", StringComparison.OrdinalIgnoreCase))
+ {
+ return " -qscale:a " + bitratePerChannel switch
+ {
+ < 40000 => "0",
+ < 56000 => "2",
+ < 80000 => "4",
+ < 112000 => "6",
+ _ => "8"
+ };
+ }
+
+ return null;
}
public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions)
@@ -5670,14 +5725,22 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var inputChannels = audioStream is null ? 6 : audioStream.Channels ?? 6;
+ var shiftAudioCodecs = new List<string>();
if (inputChannels >= 6)
{
- return;
+ // DTS and TrueHD are not supported by HLS
+ // Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used
+ shiftAudioCodecs.Add("dca");
+ shiftAudioCodecs.Add("truehd");
+ }
+ else
+ {
+ // Transcoding to 2ch ac3 or eac3 almost always causes a playback failure
+ // Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used
+ shiftAudioCodecs.Add("ac3");
+ shiftAudioCodecs.Add("eac3");
}
- // Transcoding to 2ch ac3 almost always causes a playback failure
- // Keep it in the supported codecs list, but shift it to the end of the list so that if transcoding happens, another codec is used
- var shiftAudioCodecs = new[] { "ac3", "eac3" };
if (audioCodecs.All(i => shiftAudioCodecs.Contains(i, StringComparison.OrdinalIgnoreCase)))
{
return;
@@ -5919,10 +5982,17 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var bitrate = state.OutputAudioBitrate;
-
- if (bitrate.HasValue)
+ if (bitrate.HasValue && !LosslessAudioCodecs.Contains(codec, StringComparison.OrdinalIgnoreCase))
{
- args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
+ var vbrParam = GetAudioVbrModeParam(codec, bitrate.Value / (channels ?? 2));
+ if (encodingOptions.EnableAudioVbr && vbrParam is not null)
+ {
+ args += vbrParam;
+ }
+ else
+ {
+ args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
+ }
}
if (state.OutputAudioSampleRate.HasValue)
@@ -5940,23 +6010,33 @@ namespace MediaBrowser.Controller.MediaEncoding
var audioTranscodeParams = new List<string>();
var bitrate = state.OutputAudioBitrate;
+ var channels = state.OutputAudioChannels;
+ var outputCodec = state.OutputAudioCodec;
- if (bitrate.HasValue)
+ if (bitrate.HasValue && !LosslessAudioCodecs.Contains(outputCodec, StringComparison.OrdinalIgnoreCase))
{
- audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture));
+ var vbrParam = GetAudioVbrModeParam(outputCodec, bitrate.Value / (channels ?? 2));
+ if (encodingOptions.EnableAudioVbr && vbrParam is not null)
+ {
+ audioTranscodeParams.Add(vbrParam);
+ }
+ else
+ {
+ audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture));
+ }
}
- if (state.OutputAudioChannels.HasValue)
+ if (channels.HasValue)
{
audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
}
- if (!string.IsNullOrEmpty(state.OutputAudioCodec))
+ if (!string.IsNullOrEmpty(outputCodec))
{
audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state));
}
- if (!string.Equals(state.OutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
+ if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase))
{
// opus only supports specific sampling rates
var sampleRate = state.OutputAudioSampleRate;
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 540d50bf1..3980353d1 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -25,11 +25,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
"mpeg2video",
"mpeg4",
"msmpeg4",
- "dts",
+ "dca",
"ac3",
"aac",
"mp3",
"flac",
+ "truehd",
"h264_qsv",
"hevc_qsv",
"mpeg2_qsv",
@@ -59,10 +60,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
"aac_at",
"libfdk_aac",
"ac3",
+ "dca",
"libmp3lame",
"libopus",
"libvorbis",
"flac",
+ "truehd",
"srt",
"h264_amf",
"hevc_amf",
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index f348d8417..f9f63f751 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -14,6 +14,7 @@ public class EncodingOptions
public EncodingOptions()
{
EnableFallbackFont = false;
+ EnableAudioVbr = false;
DownMixAudioBoost = 2;
DownMixStereoAlgorithm = DownMixStereoAlgorithms.None;
MaxMuxingQueueSize = 2048;
@@ -72,6 +73,11 @@ public class EncodingOptions
public bool EnableFallbackFont { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether audio VBR is enabled.
+ /// </summary>
+ public bool EnableAudioVbr { get; set; }
+
+ /// <summary>
/// Gets or sets the audio boost applied when downmixing audio.
/// </summary>
public double DownMixAudioBoost { get; set; }
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 84c8c012c..db892a22c 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -23,6 +23,9 @@ namespace MediaBrowser.Model.Dlna
private readonly ILogger _logger;
private readonly ITranscoderSupport _transcoderSupport;
+ private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc" };
+ private static readonly string[] _supportedHlsAudioCodecsTs = new string[] { "aac", "ac3", "eac3", "mp3" };
+ private static readonly string[] _supportedHlsAudioCodecsMp4 = new string[] { "aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd" };
/// <summary>
/// Initializes a new instance of the <see cref="StreamBuilder"/> class.
@@ -801,6 +804,13 @@ namespace MediaBrowser.Model.Dlna
{
// Prefer matching video codecs
var videoCodecs = ContainerProfile.SplitValue(videoCodec);
+
+ // Enforce HLS video codec restrictions
+ if (string.Equals(playlistItem.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
+ {
+ videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToArray();
+ }
+
var directVideoCodec = ContainerProfile.ContainsContainer(videoCodecs, videoStream?.Codec) ? videoStream?.Codec : null;
if (directVideoCodec is not null)
{
@@ -836,6 +846,20 @@ namespace MediaBrowser.Model.Dlna
// Prefer matching audio codecs, could do better here
var audioCodecs = ContainerProfile.SplitValue(audioCodec);
+
+ // Enforce HLS audio codec restrictions
+ if (string.Equals(playlistItem.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
+ {
+ if (string.Equals(playlistItem.Container, "mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsMp4.Contains(codec)).ToArray();
+ }
+ else
+ {
+ audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsTs.Contains(codec)).ToArray();
+ }
+ }
+
var directAudioStream = candidateAudioStreams.FirstOrDefault(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec));
playlistItem.AudioCodecs = audioCodecs;
if (directAudioStream is not null)