Merge remote-tracking branch 'upstream/master' into transcode-file-task-schedule

This commit is contained in:
crobibero 2020-11-20 08:25:45 -07:00
commit 7e7d027bb0
21 changed files with 1228 additions and 365 deletions

View file

@ -30,11 +30,11 @@ jobs:
# This is required for the SonarCloud analyzer # This is required for the SonarCloud analyzer
- task: UseDotNet@2 - task: UseDotNet@2
displayName: "Install .NET Core SDK 2.1" displayName: "Install .NET SDK 5.x"
condition: eq(variables['ImageName'], 'ubuntu-latest') condition: eq(variables['ImageName'], 'ubuntu-latest')
inputs: inputs:
packageType: sdk packageType: sdk
version: '2.1.805' version: '5.x'
- task: UseDotNet@2 - task: UseDotNet@2
displayName: "Update DotNet" displayName: "Update DotNet"

View file

@ -113,5 +113,10 @@
"TaskCleanTranscode": "Καθαρισμός Kαταλόγου Διακωδικοποιητή", "TaskCleanTranscode": "Καθαρισμός Kαταλόγου Διακωδικοποιητή",
"TaskUpdatePluginsDescription": "Κατεβάζει και εγκαθιστά ενημερώσεις για τις προσθήκες που έχουν ρυθμιστεί για αυτόματη ενημέρωση.", "TaskUpdatePluginsDescription": "Κατεβάζει και εγκαθιστά ενημερώσεις για τις προσθήκες που έχουν ρυθμιστεί για αυτόματη ενημέρωση.",
"TaskUpdatePlugins": "Ενημέρωση Προσθηκών", "TaskUpdatePlugins": "Ενημέρωση Προσθηκών",
"TaskRefreshPeopleDescription": "Ενημερώνει μεταδεδομένα για ηθοποιούς και σκηνοθέτες στην βιβλιοθήκη των πολυμέσων σας." "TaskRefreshPeopleDescription": "Ενημερώνει μεταδεδομένα για ηθοποιούς και σκηνοθέτες στην βιβλιοθήκη των πολυμέσων σας.",
"TaskCleanActivityLogDescription": "Διαγράφει καταχωρήσεις απο το αρχείο καταγραφής δραστηριοτήτων παλαιότερες από την ηλικία που έχει διαμορφωθεί.",
"TaskCleanActivityLog": "Καθαρό Αρχείο Καταγραφής Δραστηριοτήτων",
"Undefined": "Απροσδιόριστο",
"Forced": "Εξαναγκασμένο",
"Default": "Προεπιλογή"
} }

View file

@ -115,5 +115,8 @@
"TaskRefreshChannels": "Csatornák frissítése", "TaskRefreshChannels": "Csatornák frissítése",
"TaskCleanTranscodeDescription": "Törli az egy napnál régebbi átkódolási fájlokat.", "TaskCleanTranscodeDescription": "Törli az egy napnál régebbi átkódolási fájlokat.",
"TaskCleanActivityLogDescription": "A beállítottnál korábbi bejegyzések törlése a tevékenységnaplóból.", "TaskCleanActivityLogDescription": "A beállítottnál korábbi bejegyzések törlése a tevékenységnaplóból.",
"TaskCleanActivityLog": "Tevékenységnapló törlése" "TaskCleanActivityLog": "Tevékenységnapló törlése",
"Undefined": "Meghatározatlan",
"Forced": "Kényszerített",
"Default": "Alapértelmezett"
} }

View file

@ -113,5 +113,10 @@
"TasksChannelsCategory": "Kanały internetowe", "TasksChannelsCategory": "Kanały internetowe",
"TasksApplicationCategory": "Aplikacja", "TasksApplicationCategory": "Aplikacja",
"TasksLibraryCategory": "Biblioteka", "TasksLibraryCategory": "Biblioteka",
"TasksMaintenanceCategory": "Konserwacja" "TasksMaintenanceCategory": "Konserwacja",
"TaskCleanActivityLogDescription": "Usuwa wpisy dziennika aktywności starsze niż skonfigurowany wiek.",
"TaskCleanActivityLog": "Czyść dziennik aktywności",
"Undefined": "Nieustalony",
"Forced": "Wymuszony",
"Default": "Domyślne"
} }

View file

@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -41,6 +42,9 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
public class DynamicHlsController : BaseJellyfinApiController public class DynamicHlsController : BaseJellyfinApiController
{ {
private const string DefaultEncoderPreset = "veryfast";
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IDlnaManager _dlnaManager; private readonly IDlnaManager _dlnaManager;
@ -56,8 +60,7 @@ namespace Jellyfin.Api.Controllers
private readonly ILogger<DynamicHlsController> _logger; private readonly ILogger<DynamicHlsController> _logger;
private readonly EncodingHelper _encodingHelper; private readonly EncodingHelper _encodingHelper;
private readonly DynamicHlsHelper _dynamicHlsHelper; private readonly DynamicHlsHelper _dynamicHlsHelper;
private readonly EncodingOptions _encodingOptions;
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DynamicHlsController"/> class. /// Initializes a new instance of the <see cref="DynamicHlsController"/> class.
@ -92,6 +95,8 @@ namespace Jellyfin.Api.Controllers
ILogger<DynamicHlsController> logger, ILogger<DynamicHlsController> logger,
DynamicHlsHelper dynamicHlsHelper) DynamicHlsHelper dynamicHlsHelper)
{ {
_encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
_libraryManager = libraryManager; _libraryManager = libraryManager;
_userManager = userManager; _userManager = userManager;
_dlnaManager = dlnaManager; _dlnaManager = dlnaManager;
@ -106,8 +111,7 @@ namespace Jellyfin.Api.Controllers
_transcodingJobHelper = transcodingJobHelper; _transcodingJobHelper = transcodingJobHelper;
_logger = logger; _logger = logger;
_dynamicHlsHelper = dynamicHlsHelper; _dynamicHlsHelper = dynamicHlsHelper;
_encodingOptions = serverConfigurationManager.GetEncodingOptions();
_encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
} }
/// <summary> /// <summary>
@ -272,7 +276,7 @@ namespace Jellyfin.Api.Controllers
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
}; };
return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@ -439,7 +443,7 @@ namespace Jellyfin.Api.Controllers
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
}; };
return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@ -1129,7 +1133,7 @@ namespace Jellyfin.Api.Controllers
_dlnaManager, _dlnaManager,
_deviceManager, _deviceManager,
_transcodingJobHelper, _transcodingJobHelper,
_transcodingJobType, TranscodingJobType,
cancellationTokenSource.Token) cancellationTokenSource.Token)
.ConfigureAwait(false); .ConfigureAwait(false);
@ -1137,11 +1141,19 @@ namespace Jellyfin.Api.Controllers
var segmentLengths = GetSegmentLengths(state); 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(); var builder = new StringBuilder();
builder.AppendLine("#EXTM3U") builder.AppendLine("#EXTM3U")
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD") .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
.AppendLine("#EXT-X-VERSION:3") .Append("#EXT-X-VERSION:")
.Append(hlsVersion)
.AppendLine()
.Append("#EXT-X-TARGETDURATION:") .Append("#EXT-X-TARGETDURATION:")
.Append(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength)) .Append(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength))
.AppendLine() .AppendLine()
@ -1151,6 +1163,18 @@ namespace Jellyfin.Api.Controllers
var segmentExtension = GetSegmentFileExtension(streamingRequest.SegmentContainer); var segmentExtension = GetSegmentFileExtension(streamingRequest.SegmentContainer);
var queryString = Request.QueryString; 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) foreach (var length in segmentLengths)
{ {
builder.Append("#EXTINF:") builder.Append("#EXTINF:")
@ -1194,7 +1218,7 @@ namespace Jellyfin.Api.Controllers
_dlnaManager, _dlnaManager,
_deviceManager, _deviceManager,
_transcodingJobHelper, _transcodingJobHelper,
_transcodingJobType, TranscodingJobType,
cancellationTokenSource.Token) cancellationTokenSource.Token)
.ConfigureAwait(false); .ConfigureAwait(false);
@ -1208,7 +1232,7 @@ namespace Jellyfin.Api.Controllers
if (System.IO.File.Exists(segmentPath)) 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); _logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false); 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)) if (System.IO.File.Exists(segmentPath))
{ {
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType); job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
transcodingLock.Release(); transcodingLock.Release();
released = true; released = true;
_logger.LogDebug("returning {0} [it exists, try 2]", segmentPath); _logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
@ -1233,7 +1257,13 @@ namespace Jellyfin.Api.Controllers
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength; 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"); _logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
startTranscoding = true; startTranscoding = true;
@ -1265,13 +1295,12 @@ namespace Jellyfin.Api.Controllers
streamingRequest.StartTimeTicks = GetStartPositionTicks(state, segmentId); streamingRequest.StartTimeTicks = GetStartPositionTicks(state, segmentId);
state.WaitForPath = segmentPath; state.WaitForPath = segmentPath;
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
job = await _transcodingJobHelper.StartFfMpeg( job = await _transcodingJobHelper.StartFfMpeg(
state, state,
playlistPath, playlistPath,
GetCommandLineArguments(playlistPath, encodingOptions, state, true, segmentId), GetCommandLineArguments(playlistPath, state, true, segmentId),
Request, Request,
_transcodingJobType, TranscodingJobType,
cancellationTokenSource).ConfigureAwait(false); cancellationTokenSource).ConfigureAwait(false);
} }
catch catch
@ -1284,7 +1313,7 @@ namespace Jellyfin.Api.Controllers
} }
else else
{ {
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType); job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
if (job?.TranscodingThrottler != null) if (job?.TranscodingThrottler != null)
{ {
await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false); await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
@ -1301,7 +1330,7 @@ namespace Jellyfin.Api.Controllers
} }
_logger.LogDebug("returning {0} [general case]", segmentPath); _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); return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
} }
@ -1325,11 +1354,10 @@ namespace Jellyfin.Api.Controllers
return result.ToArray(); 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 videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec); // GetNumberOfThreads is static.
if (state.BaseRequest.BreakOnNonKeyFrames) if (state.BaseRequest.BreakOnNonKeyFrames)
{ {
@ -1341,36 +1369,57 @@ namespace Jellyfin.Api.Controllers
state.BaseRequest.BreakOnNonKeyFrames = false; state.BaseRequest.BreakOnNonKeyFrames = false;
} }
var inputModifier = _encodingHelper.GetInputModifier(state, encodingOptions);
// If isEncoding is true we're actually starting ffmpeg // If isEncoding is true we're actually starting ffmpeg
var startNumberParam = isEncoding ? startNumber.ToString(CultureInfo.InvariantCulture) : "0"; var startNumberParam = isEncoding ? startNumber.ToString(CultureInfo.InvariantCulture) : "0";
var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty; 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 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 = outputExtension.TrimStart('.');
var segmentFormat = GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase)) if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
{ {
segmentFormat = "mpegts"; 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 segmentFormat = "fmp4" + outputFmp4HeaderArg;
? encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture) }
else
{
_logger.LogError("Invalid HLS segment container: " + segmentFormat);
}
var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128
? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
: "128"; : "128";
return string.Format( return string.Format(
CultureInfo.InvariantCulture, 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, inputModifier,
_encodingHelper.GetInputArgument(state, encodingOptions), _encodingHelper.GetInputArgument(state, _encodingOptions),
threads, threads,
mapArgs, mapArgs,
GetVideoArguments(state, encodingOptions, startNumber), GetVideoArguments(state, startNumber),
GetAudioArguments(state, encodingOptions), GetAudioArguments(state),
maxMuxingQueueSize, maxMuxingQueueSize,
state.SegmentLength.ToString(CultureInfo.InvariantCulture), state.SegmentLength.ToString(CultureInfo.InvariantCulture),
segmentFormat, segmentFormat,
@ -1379,50 +1428,63 @@ namespace Jellyfin.Api.Controllers
outputPath).Trim(); 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); var audioCodec = _encodingHelper.GetAudioEncoder(state);
if (!state.IsOutputVideo) if (!state.IsOutputVideo)
{ {
if (EncodingHelper.IsCopyCodec(audioCodec)) 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) 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) 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) if (state.OutputAudioSampleRate.HasValue)
{ {
audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)); audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
} }
audioTranscodeParams.Add("-vn"); audioTranscodeParams += " -vn";
return string.Join(' ', audioTranscodeParams); return audioTranscodeParams;
} }
if (EncodingHelper.IsCopyCodec(audioCodec)) 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)) 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; var args = "-codec:a:0 " + audioCodec;
@ -1446,94 +1508,89 @@ namespace Jellyfin.Api.Controllers
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
} }
args += " " + _encodingHelper.GetAudioFilterParam(state, encodingOptions, true); args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
return args; 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) if (!state.IsOutputVideo)
{ {
return string.Empty; return string.Empty;
} }
var codec = _encodingHelper.GetVideoEncoder(state, encodingOptions); var codec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
var args = "-codec:v:0 " + codec; 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) // if (state.EnableMpegtsM2TsMode)
// { // {
// args += " -mpegts_m2ts_mode 1"; // 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 (EncodingHelper.IsCopyCodec(codec))
{ {
if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) 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)) if (!string.IsNullOrEmpty(bitStreamArgs))
{ {
args += " " + bitStreamArgs; args += " " + bitStreamArgs;
} }
} }
args += " -start_at_zero";
// args += " -flags -global_header"; // args += " -flags -global_header";
} }
else else
{ {
var gopArg = string.Empty; args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset);
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; // Set the key frame params for video encoding to match the hls segment time.
args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, false, startNumber);
if (framerate.HasValue) // Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
{ {
// This is to make sure keyframe interval is limited to our segment, args += " -bf 0";
// as forcing keyframes is not enough.
// Example: we encoded half of desired length, then codec detected
// scene cut and inserted a keyframe; next forced keyframe would
// be created outside of segment, which breaks seeking
// -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe
gopArg = string.Format(
CultureInfo.InvariantCulture,
" -g {0} -keyint_min {0} -sc_threshold 0",
Math.Ceiling(state.SegmentLength * framerate.Value));
}
args += " " + _encodingHelper.GetVideoQualityParam(state, codec, encodingOptions, "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))
{
args += " " + gopArg;
}
else
{
args += " " + keyFrameArg + gopArg;
} }
// args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=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; var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
// This is for graphical subs
if (hasGraphicalSubs) 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 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, // -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) private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
{ {
var job = _transcodingJobHelper.GetTranscodingJob(playlist, _transcodingJobType); var job = _transcodingJobHelper.GetTranscodingJob(playlist, TranscodingJobType);
if (job == null || job.HasExited) if (job == null || job.HasExited)
{ {

View file

@ -191,8 +191,11 @@ namespace Jellyfin.Api.Controllers
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
{ {
// hls segment container can only be mpegts or fmp4 per ffmpeg documentation // 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 // 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 var dynamicHlsRequestDto = new HlsAudioRequestDto
{ {
@ -201,7 +204,7 @@ namespace Jellyfin.Api.Controllers
Static = isStatic, Static = isStatic,
PlaySessionId = info.PlaySessionId, PlaySessionId = info.PlaySessionId,
// fallback to mpegts if device reports some weird value unsupported by hls // 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, MediaSourceId = mediaSourceId,
DeviceId = deviceId, DeviceId = deviceId,
AudioCodec = audioCodec, AudioCodec = audioCodec,

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
@ -296,23 +297,23 @@ namespace Jellyfin.Api.Controllers
.ConfigureAwait(false); .ConfigureAwait(false);
TranscodingJobDto? job = null; 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); await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
try try
{ {
if (!System.IO.File.Exists(playlist)) if (!System.IO.File.Exists(playlistPath))
{ {
// If the playlist doesn't already exist, startup ffmpeg // If the playlist doesn't already exist, startup ffmpeg
try try
{ {
job = await _transcodingJobHelper.StartFfMpeg( job = await _transcodingJobHelper.StartFfMpeg(
state, state,
playlist, playlistPath,
GetCommandLineArguments(playlist, state), GetCommandLineArguments(playlistPath, state),
Request, Request,
TranscodingJobType, TranscodingJobType,
cancellationTokenSource) cancellationTokenSource)
@ -328,7 +329,7 @@ namespace Jellyfin.Api.Controllers
minSegments = state.MinSegments; minSegments = state.MinSegments;
if (minSegments > 0) 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) if (job != null)
{ {
_transcodingJobHelper.OnTranscodeEndRequest(job); _transcodingJobHelper.OnTranscodeEndRequest(job);
} }
var playlistText = HlsHelpers.GetLivePlaylistText(playlist, state.SegmentLength); var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state);
return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8")); return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8"));
} }
@ -361,15 +362,44 @@ namespace Jellyfin.Api.Controllers
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec); // GetNumberOfThreads is static. var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec); // GetNumberOfThreads is static.
var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions); 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 segmentFormat = format.TrimStart('.'); 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);
var outputTsArg = outputPrefix + "%d" + outputExtension;
var segmentFormat = outputExtension.TrimStart('.');
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase)) if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
{ {
segmentFormat = "mpegts"; 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( var baseUrlParam = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
@ -378,20 +408,19 @@ namespace Jellyfin.Api.Controllers
return string.Format( return string.Format(
CultureInfo.InvariantCulture, 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, inputModifier,
_encodingHelper.GetInputArgument(state, _encodingOptions), _encodingHelper.GetInputArgument(state, _encodingOptions),
threads, threads,
_encodingHelper.GetMapArgs(state), mapArgs,
GetVideoArguments(state), GetVideoArguments(state),
GetAudioArguments(state), GetAudioArguments(state),
maxMuxingQueueSize,
state.SegmentLength.ToString(CultureInfo.InvariantCulture), state.SegmentLength.ToString(CultureInfo.InvariantCulture),
string.Empty,
segmentFormat, segmentFormat,
baseUrlParam, baseUrlParam,
outputPath, outputTsArg,
outputTsArg) outputPath).Trim();
.Trim();
} }
/// <summary> /// <summary>
@ -401,14 +430,53 @@ namespace Jellyfin.Api.Controllers
/// <returns>The command line arguments for audio transcoding.</returns> /// <returns>The command line arguments for audio transcoding.</returns>
private string GetAudioArguments(StreamState state) private string GetAudioArguments(StreamState state)
{ {
var codec = _encodingHelper.GetAudioEncoder(state); if (state.AudioStream == null)
if (EncodingHelper.IsCopyCodec(codec))
{ {
return "-codec:a:0 copy"; return string.Empty;
} }
var args = "-codec:a:0 " + 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))
{
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
return "-acodec copy -strict -2" + bitStreamArgs;
}
var args = "-codec:a:0 " + audioCodec;
var channels = state.OutputAudioChannels; var channels = state.OutputAudioChannels;
@ -429,7 +497,7 @@ namespace Jellyfin.Api.Controllers
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
} }
args += " " + _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true); args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
return args; return args;
} }
@ -441,6 +509,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>The command line arguments for video transcoding.</returns> /// <returns>The command line arguments for video transcoding.</returns>
private string GetVideoArguments(StreamState state) private string GetVideoArguments(StreamState state)
{ {
if (state.VideoStream == null)
{
return string.Empty;
}
if (!state.IsOutputVideo) if (!state.IsOutputVideo)
{ {
return string.Empty; return string.Empty;
@ -450,46 +523,64 @@ namespace Jellyfin.Api.Controllers
var args = "-codec:v:0 " + codec; 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) // if (state.EnableMpegtsM2TsMode)
// { // {
// args += " -mpegts_m2ts_mode 1"; // 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 (codec.Equals("copy", StringComparison.OrdinalIgnoreCase)) if (EncodingHelper.IsCopyCodec(codec))
{ {
// if h264_mp4toannexb is ever added, do not use it for live tv // If h264_mp4toannexb is ever added, do not use it for live tv.
if (state.VideoStream != null && if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
!string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
{ {
string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream); string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
if (!string.IsNullOrEmpty(bitStreamArgs)) if (!string.IsNullOrEmpty(bitStreamArgs))
{ {
args += " " + bitStreamArgs; args += " " + bitStreamArgs;
} }
} }
args += " -start_at_zero";
} }
else else
{ {
var keyFrameArg = string.Format( args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset);
CultureInfo.InvariantCulture,
" -force_key_frames \"expr:gte(t,n_forced*{0})\"", // Set the key frame params for video encoding to match the hls segment time.
state.SegmentLength.ToString(CultureInfo.InvariantCulture)); args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, true, null);
// Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
{
args += " -bf 0";
}
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
args += " " + _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset) + keyFrameArg; if (hasGraphicalSubs)
// Add resolution params, if specified
if (!hasGraphicalSubs)
{ {
// Graphical subs overlay and resolution params.
args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec);
}
else
{
// Resolution params.
args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec); args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec);
} }
// This is for internal graphical subs if (state.SubtitleStream == null || !state.SubtitleStream.IsExternal || state.SubtitleStream.IsTextSubtitleStream)
if (hasGraphicalSubs)
{ {
args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec); args += " -start_at_zero";
} }
} }

View file

@ -207,7 +207,61 @@ namespace Jellyfin.Api.Helpers
AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User); 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())) if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIp()))
{ {
@ -217,40 +271,77 @@ namespace Jellyfin.Api.Helpers
var variation = GetBitrateVariation(totalBitrate); var variation = GetBitrateVariation(totalBitrate);
var newBitrate = totalBitrate - variation; var newBitrate = totalBitrate - variation;
var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
variation *= 2; variation *= 2;
newBitrate = totalBitrate - variation; newBitrate = totalBitrate - variation;
variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
} }
return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); 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(bitrate.ToString(CultureInfo.InvariantCulture))
.Append(",AVERAGE-BANDWIDTH=") .Append(",AVERAGE-BANDWIDTH=")
.Append(bitrate.ToString(CultureInfo.InvariantCulture)); .Append(bitrate.ToString(CultureInfo.InvariantCulture));
AppendPlaylistCodecsField(builder, state); AppendPlaylistVideoRangeField(playlistBuilder, state);
AppendPlaylistResolutionField(builder, state); AppendPlaylistCodecsField(playlistBuilder, state);
AppendPlaylistFramerateField(builder, state); AppendPlaylistResolutionField(playlistBuilder, state);
AppendPlaylistFramerateField(playlistBuilder, state);
if (!string.IsNullOrWhiteSpace(subtitleGroup)) if (!string.IsNullOrWhiteSpace(subtitleGroup))
{ {
builder.Append(",SUBTITLES=\"") playlistBuilder.Append(",SUBTITLES=\"")
.Append(subtitleGroup) .Append(subtitleGroup)
.Append('"'); .Append('"');
} }
builder.Append(Environment.NewLine); playlistBuilder.Append(Environment.NewLine);
builder.AppendLine(url); 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> /// <summary>
@ -419,15 +510,27 @@ namespace Jellyfin.Api.Helpers
/// <returns>H.26X level of the output video stream.</returns> /// <returns>H.26X level of the output video stream.</returns>
private int? GetOutputVideoCodecLevel(StreamState state) private int? GetOutputVideoCodecLevel(StreamState state)
{ {
string? levelString; string levelString = string.Empty;
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& state.VideoStream != null
&& state.VideoStream.Level.HasValue) && state.VideoStream.Level.HasValue)
{ {
levelString = state.VideoStream?.Level.ToString(); levelString = state.VideoStream.Level.ToString() ?? string.Empty;
} }
else 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)) if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
@ -438,6 +541,38 @@ namespace Jellyfin.Api.Helpers
return null; 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> /// <summary>
/// Gets a formatted string of the output audio codec, for use in the CODECS field. /// Gets a formatted string of the output audio codec, for use in the CODECS field.
/// </summary> /// </summary>
@ -468,6 +603,16 @@ namespace Jellyfin.Api.Helpers
return HlsCodecStringHelpers.GetEAC3String(); 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; return string.Empty;
} }
@ -492,15 +637,14 @@ namespace Jellyfin.Api.Helpers
if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase)) if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
{ {
string? profile = state.GetRequestedProfiles("h264").FirstOrDefault(); string profile = GetOutputVideoCodecProfile(state, "h264");
return HlsCodecStringHelpers.GetH264String(profile, level); return HlsCodecStringHelpers.GetH264String(profile, level);
} }
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{ {
string? profile = state.GetRequestedProfiles("h265").FirstOrDefault(); string profile = GetOutputVideoCodecProfile(state, "hevc");
return HlsCodecStringHelpers.GetH265String(profile, level); return HlsCodecStringHelpers.GetH265String(profile, level);
} }
@ -544,12 +688,30 @@ namespace Jellyfin.Api.Helpers
return variation; return variation;
} }
private string ReplaceBitrate(string url, int oldValue, int newValue) private string ReplaceVideoBitrate(string url, int oldValue, int newValue)
{ {
return url.Replace( return url.Replace(
"videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture), "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
"videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture), "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
StringComparison.OrdinalIgnoreCase); 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);
}
} }
} }

View file

@ -9,13 +9,38 @@ namespace Jellyfin.Api.Helpers
/// </summary> /// </summary>
public static class HlsCodecStringHelpers 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> /// <summary>
/// Gets a MP3 codec string. /// Gets a MP3 codec string.
/// </summary> /// </summary>
/// <returns>MP3 codec string.</returns> /// <returns>MP3 codec string.</returns>
public static string GetMP3String() public static string GetMP3String()
{ {
return "mp4a.40.34"; return MP3;
} }
/// <summary> /// <summary>
@ -40,6 +65,42 @@ namespace Jellyfin.Api.Helpers
return result.ToString(); 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> /// <summary>
/// Gets a H.264 codec string. /// Gets a H.264 codec string.
/// </summary> /// </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. // 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: // This is what I've found through various sources:
// FORMAT: [codecTag].[profile].[constraint?].L[level * 30].[UNKNOWN] // 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 else
{ {
// Default to main if profile is invalid // Default to main if profile is invalid
result.Append(".1.6"); result.Append(".1.4");
} }
result.Append(".L") result.Append(".L")
.Append(level * 3) .Append(level)
.Append(".B0"); .Append(".B0");
return result.ToString(); 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";
}
} }
} }

View file

@ -1,8 +1,11 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging; 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> /// <summary>
/// Gets the hls playlist text. /// Gets the hls playlist text.
/// </summary> /// </summary>
/// <param name="path">The path to the playlist file.</param> /// <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> /// <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 stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(stream); using var reader = new StreamReader(stream);
var text = reader.ReadToEnd(); var text = reader.ReadToEnd();
text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT", StringComparison.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);
var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture); // Replace fMP4 init file URI.
text = text.Replace(fmp4InitFileName, newFmp4InitFileName, StringComparison.InvariantCulture);
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);
return text; return text;
} }

View file

@ -169,7 +169,9 @@ namespace Jellyfin.Api.Helpers
state.DirectStreamProvider = liveStreamInfo.Item2; 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); string? containerInternal = Path.GetExtension(state.RequestedUrl);
@ -187,7 +189,7 @@ namespace Jellyfin.Api.Helpers
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.'); 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; state.OutputAudioCodec = streamingRequest.AudioCodec;
@ -200,20 +202,41 @@ namespace Jellyfin.Api.Helpers
encodingHelper.TryStreamCopy(state); encodingHelper.TryStreamCopy(state);
if (state.OutputVideoBitrate.HasValue && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue)
{ {
var resolution = ResolutionNormalizer.Normalize( var isVideoResolutionNotRequested = !state.VideoRequest.Width.HasValue
state.VideoStream?.BitRate, && !state.VideoRequest.Height.HasValue
state.VideoStream?.Width, && !state.VideoRequest.MaxWidth.HasValue
state.VideoStream?.Height, && !state.VideoRequest.MaxHeight.HasValue;
state.OutputVideoBitrate.Value,
state.VideoStream?.Codec,
state.OutputVideoCodec,
state.VideoRequest.MaxWidth,
state.VideoRequest.MaxHeight);
state.VideoRequest.MaxWidth = resolution.MaxWidth; if (isVideoResolutionNotRequested
state.VideoRequest.MaxHeight = resolution.MaxHeight; && 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;
}
} }
} }

View file

@ -771,8 +771,9 @@ namespace Jellyfin.Api.Helpers
new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken }, new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
cancellationTokenSource.Token) cancellationTokenSource.Token)
.ConfigureAwait(false); .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) if (state.VideoRequest != null)
{ {

View file

@ -0,0 +1,24 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MediaBrowser.Common.Json.Converters
{
/// <summary>
/// Returns an ISO8601 formatted datetime.
/// </summary>
/// <remarks>
/// Used for legacy compatibility.
/// </remarks>
public class JsonDateTimeIso8601Converter : JsonConverter<DateTime>
{
/// <inheritdoc />
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> reader.GetDateTime();
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString("O", CultureInfo.InvariantCulture));
}
}

View file

@ -42,6 +42,7 @@ namespace MediaBrowser.Common.Json
options.Converters.Add(new JsonGuidConverter()); options.Converters.Add(new JsonGuidConverter());
options.Converters.Add(new JsonStringEnumConverter()); options.Converters.Add(new JsonStringEnumConverter());
options.Converters.Add(new JsonNullableStructConverterFactory()); options.Converters.Add(new JsonNullableStructConverterFactory());
options.Converters.Add(new JsonDateTimeIso8601Converter());
return options; return options;
} }

View file

@ -7,6 +7,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -23,7 +24,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
public class EncodingHelper 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 IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
@ -440,6 +441,12 @@ namespace MediaBrowser.Controller.MediaEncoding
return "libopus"; return "libopus";
} }
if (string.Equals(codec, "flac", StringComparison.OrdinalIgnoreCase))
{
// flac is experimental in mp4 muxer
return "flac -strict -2";
}
return codec.ToLowerInvariant(); return codec.ToLowerInvariant();
} }
@ -573,7 +580,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// </summary> /// </summary>
/// <param name="stream">The stream.</param> /// <param name="stream">The stream.</param>
/// <returns><c>true</c> if the specified stream is H264; otherwise, <c>false</c>.</returns> /// <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; var codec = stream.Codec ?? string.Empty;
@ -581,7 +588,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|| codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1; || codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
} }
public bool IsH265(MediaStream stream) public static bool IsH265(MediaStream stream)
{ {
var codec = stream.Codec ?? string.Empty; var codec = stream.Codec ?? string.Empty;
@ -589,10 +596,17 @@ namespace MediaBrowser.Controller.MediaEncoding
|| codec.IndexOf("hevc", StringComparison.OrdinalIgnoreCase) != -1; || codec.IndexOf("hevc", StringComparison.OrdinalIgnoreCase) != -1;
} }
// TODO This is auto inserted into the mpegts mux so it might not be needed public static bool IsAAC(MediaStream stream)
// https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb
public string GetBitStreamArgs(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)) if (IsH264(stream))
{ {
return "-bsf:v h264_mp4toannexb"; return "-bsf:v h264_mp4toannexb";
@ -601,12 +615,44 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
return "-bsf:v hevc_mp4toannexb"; return "-bsf:v hevc_mp4toannexb";
} }
else if (IsAAC(stream))
{
// Convert adts header(mpegts) to asc header(mp4).
return "-bsf:a aac_adtstoasc";
}
else else
{ {
return null; 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) public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec)
{ {
var bitrate = state.OutputVideoBitrate; var bitrate = state.OutputVideoBitrate;
@ -654,16 +700,30 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Empty; 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))
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)))
{ {
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; return level;
@ -766,6 +826,72 @@ namespace MediaBrowser.Controller.MediaEncoding
return null; 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> /// <summary>
/// Gets the video bitrate to specify on the command line. /// Gets the video bitrate to specify on the command line.
/// </summary> /// </summary>
@ -773,6 +899,47 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
var param = string.Empty; 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 && var isVc1 = state.VideoStream != null &&
string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase); string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase); var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase);
@ -781,11 +948,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
if (!string.IsNullOrEmpty(encodingOptions.EncoderPreset)) if (!string.IsNullOrEmpty(encodingOptions.EncoderPreset))
{ {
param += "-preset " + encodingOptions.EncoderPreset; param += " -preset " + encodingOptions.EncoderPreset;
} }
else else
{ {
param += "-preset " + defaultPreset; param += " -preset " + defaultPreset;
} }
int encodeCrf = encodingOptions.H264Crf; int encodeCrf = encodingOptions.H264Crf;
@ -809,38 +976,40 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -crf " + defaultCrf; 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" }; string[] valid_h264_qsv = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" };
if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparer.OrdinalIgnoreCase)) if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparer.OrdinalIgnoreCase))
{ {
param += "-preset " + encodingOptions.EncoderPreset; param += " -preset " + encodingOptions.EncoderPreset;
} }
else else
{ {
param += "-preset 7"; param += " -preset 7";
} }
param += " -look_ahead 0"; param += " -look_ahead 0";
} }
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc) 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) switch (encodingOptions.EncoderPreset)
{ {
case "veryslow": 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; break;
case "slow": case "slow":
case "slower": case "slower":
param += "-preset slow"; param += " -preset slow";
break; break;
case "medium": case "medium":
param += "-preset medium"; param += " -preset medium";
break; break;
case "fast": case "fast":
@ -848,27 +1017,27 @@ namespace MediaBrowser.Controller.MediaEncoding
case "veryfast": case "veryfast":
case "superfast": case "superfast":
case "ultrafast": case "ultrafast":
param += "-preset fast"; param += " -preset fast";
break; break;
default: default:
param += "-preset default"; param += " -preset default";
break; break;
} }
} }
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf)
|| string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_amf)
{ {
switch (encodingOptions.EncoderPreset) switch (encodingOptions.EncoderPreset)
{ {
case "veryslow": case "veryslow":
case "slow": case "slow":
case "slower": case "slower":
param += "-quality quality"; param += " -quality quality";
break; break;
case "medium": case "medium":
param += "-quality balanced"; param += " -quality balanced";
break; break;
case "fast": case "fast":
@ -876,11 +1045,11 @@ namespace MediaBrowser.Controller.MediaEncoding
case "veryfast": case "veryfast":
case "superfast": case "superfast":
case "ultrafast": case "ultrafast":
param += "-quality speed"; param += " -quality speed";
break; break;
default: default:
param += "-quality speed"; param += " -quality speed";
break; break;
} }
@ -896,6 +1065,11 @@ namespace MediaBrowser.Controller.MediaEncoding
// Enhance workload when tone mapping with AMF on some APUs // Enhance workload when tone mapping with AMF on some APUs
param += " -preanalysis true"; 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 else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm
{ {
@ -917,7 +1091,7 @@ namespace MediaBrowser.Controller.MediaEncoding
profileScore = Math.Min(profileScore, 2); profileScore = Math.Min(profileScore, 2);
// http://www.webmproject.org/docs/encoder-parameters/ // 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), profileScore.ToString(_usCulture),
crf, crf,
qmin, qmin,
@ -925,15 +1099,15 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
else if (string.Equals(videoEncoder, "mpeg4", StringComparison.OrdinalIgnoreCase)) 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 else if (string.Equals(videoEncoder, "wmv2", StringComparison.OrdinalIgnoreCase)) // asf/wmv
{ {
param += "-qmin 2"; param += " -qmin 2";
} }
else if (string.Equals(videoEncoder, "msmpeg4", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(videoEncoder, "msmpeg4", StringComparison.OrdinalIgnoreCase))
{ {
param += "-mbd 2"; param += " -mbd 2";
} }
param += GetVideoBitrateParam(state, videoEncoder); param += GetVideoBitrateParam(state, videoEncoder);
@ -945,11 +1119,25 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
var targetVideoCodec = state.ActualOutputVideoCodec; 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(); 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, // Only libx264 support encoding H264 High 10 Profile, otherwise force High Profile.
// which is compatible (and ugly) 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) if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
&& profile != null && profile != null
&& profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1) && profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1)
@ -957,13 +1145,31 @@ namespace MediaBrowser.Controller.MediaEncoding
profile = "constrained_baseline"; 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.IsNullOrEmpty(profile))
{ {
if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
{ {
// not supported by h264_omx // 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)) 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 // libx264, QSV, AMF, VAAPI can adjust the given level to match the output.
// also needed for libx264 due to https://trac.ffmpeg.org/ticket/3307
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
|| string.Equals(videoEncoder, "libx265", 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 " + hevcLevel / 3;
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;
} }
} }
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
{ {
// nvenc doesn't decode with param -level set ?! param += " -level " + level;
// TODO:
} }
else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase))
{
// 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)
|| !string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
{ {
param += " -level " + level; param += " -level " + level;
} }
@ -1032,42 +1218,11 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase)) if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
{ {
// todo // libx265 only accept level option in -x265-params.
} // level option may cause libx265 to fail.
// libx265 cannot adjust the given level, just throw an error.
if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) // TODO: set fine tuned params.
&& !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) param += " -x265-params:0 no-info=1";
&& !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;
} }
return param; return param;
@ -1346,7 +1501,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)) || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
{ {
return .5; return .6;
} }
return 1; return 1;
@ -1380,36 +1535,48 @@ namespace MediaBrowser.Controller.MediaEncoding
public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream) public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream)
{ {
if (audioStream == null) return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream);
{
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;
} }
public int? GetAudioBitrateParam(int? audioBitRate, MediaStream audioStream) public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream)
{ {
if (audioStream == null) if (audioStream == null)
{ {
return 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); 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 // Empty bitrate area is not allow on iOS
// Default audio bitrate to 128K if it is not being requested // Default audio bitrate to 128K if it is not being requested
// https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options // https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
@ -1447,7 +1614,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (filters.Count > 0) if (filters.Count > 0)
{ {
return "-af \"" + string.Join(",", filters) + "\""; return " -af \"" + string.Join(",", filters) + "\"";
} }
return string.Empty; return string.Empty;
@ -1462,6 +1629,11 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <returns>System.Nullable{System.Int32}.</returns> /// <returns>System.Nullable{System.Int32}.</returns>
public int? GetNumAudioChannelsParam(EncodingJobInfo state, MediaStream audioStream, string outputAudioCodec) public int? GetNumAudioChannelsParam(EncodingJobInfo state, MediaStream audioStream, string outputAudioCodec)
{ {
if (audioStream == null)
{
return null;
}
var request = state.BaseRequest; var request = state.BaseRequest;
var inputChannels = audioStream?.Channels; var inputChannels = audioStream?.Channels;
@ -1484,6 +1656,11 @@ namespace MediaBrowser.Controller.MediaEncoding
// libmp3lame currently only supports two channel output // libmp3lame currently only supports two channel output
transcoderChannelLimit = 2; transcoderChannelLimit = 2;
} }
else if (codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1)
{
// aac is able to handle 8ch(7.1 layout)
transcoderChannelLimit = 8;
}
else 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 // 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 // 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"; videoSizeParam += ",hwupload=extra_hw_frames=64";
} }
@ -1729,7 +1907,8 @@ namespace MediaBrowser.Controller.MediaEncoding
: " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\""; : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"";
// When the input may or may not be hardware VAAPI decodable // 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 [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 // 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 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 [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\""; 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. QSV in FFMpeg can now setup hardware overlay for transcodes.
@ -1776,7 +1957,7 @@ namespace MediaBrowser.Controller.MediaEncoding
videoSizeParam); videoSizeParam);
} }
private (int? width, int? height) GetFixedOutputSize( public static (int? width, int? height) GetFixedOutputSize(
int? videoWidth, int? videoWidth,
int? videoHeight, int? videoHeight,
int? requestedWidth, int? requestedWidth,
@ -1836,7 +2017,9 @@ namespace MediaBrowser.Controller.MediaEncoding
requestedMaxHeight); requestedMaxHeight);
if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) 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 && width.HasValue
&& height.HasValue) && height.HasValue)
{ {
@ -1845,7 +2028,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// output dimensions. Output dimensions are guaranteed to be even. // output dimensions. Output dimensions are guaranteed to be even.
var outputWidth = width.Value; var outputWidth = width.Value;
var outputHeight = height.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) var isDeintEnabled = state.DeInterlace("h264", true)
|| state.DeInterlace("avc", true) || state.DeInterlace("avc", true)
|| state.DeInterlace("h265", true) || state.DeInterlace("h265", true)
@ -2107,10 +2291,13 @@ namespace MediaBrowser.Controller.MediaEncoding
var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1; var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_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 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 isNvdecH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
var isNvdecHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1; var isNvdecHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", 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 isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
var isColorDepth10 = IsColorDepth10(state); var isColorDepth10 = IsColorDepth10(state);
@ -2185,6 +2372,7 @@ namespace MediaBrowser.Controller.MediaEncoding
filters.Add("hwdownload"); filters.Add("hwdownload");
if (isLibX264Encoder if (isLibX264Encoder
|| isLibX265Encoder
|| hasGraphicalSubs || hasGraphicalSubs
|| (isNvdecHevcDecoder && isDeinterlaceHevc) || (isNvdecHevcDecoder && isDeinterlaceHevc)
|| (!isNvdecHevcDecoder && isDeinterlaceH264 || isDeinterlaceHevc)) || (!isNvdecHevcDecoder && isDeinterlaceH264 || isDeinterlaceHevc))
@ -2195,20 +2383,20 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
// When the input may or may not be hardware VAAPI decodable // When the input may or may not be hardware VAAPI decodable
if (isVaapiH264Encoder) if (isVaapiH264Encoder || isVaapiHevcEncoder)
{ {
filters.Add("format=nv12|vaapi"); filters.Add("format=nv12|vaapi");
filters.Add("hwupload"); filters.Add("hwupload");
} }
// When burning in graphical subtitles using overlay_qsv, upload videostream to the same qsv context // 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"); filters.Add("hwupload=extra_hw_frames=64");
} }
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first // 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(); var codec = videoStream.Codec.ToLowerInvariant();
@ -2250,7 +2438,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// Add software deinterlace filter before scaling filter // Add software deinterlace filter before scaling filter
if ((isDeinterlaceH264 || isDeinterlaceHevc) if ((isDeinterlaceH264 || isDeinterlaceHevc)
&& !isVaapiH264Encoder && !isVaapiH264Encoder
&& !isVaapiHevcEncoder
&& !isQsvH264Encoder && !isQsvH264Encoder
&& !isQsvHevcEncoder
&& !isNvdecH264Decoder) && !isNvdecH264Decoder)
{ {
if (string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase)) 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) // Add parameters to use VAAPI with burn-in text subtitles (GH issue #642)
if (isVaapiH264Encoder) if (isVaapiH264Encoder || isVaapiHevcEncoder)
{ {
if (hasTextSubs) if (hasTextSubs)
{ {
@ -2562,6 +2752,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public void AttachMediaSourceInfo( public void AttachMediaSourceInfo(
EncodingJobInfo state, EncodingJobInfo state,
EncodingOptions encodingOptions,
MediaSourceInfo mediaSource, MediaSourceInfo mediaSource,
string requestedUrl) string requestedUrl)
{ {
@ -2692,11 +2883,23 @@ namespace MediaBrowser.Controller.MediaEncoding
request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => _mediaEncoder.CanEncodeToAudioCodec(i)) request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => _mediaEncoder.CanEncodeToAudioCodec(i))
?? state.SupportedAudioCodecs.FirstOrDefault(); ?? 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) 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) if (audioCodecs.Count < 2)
{ {
return; 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) private void NormalizeSubtitleEmbed(EncodingJobInfo state)
{ {
if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed) if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed)
@ -3357,7 +3588,7 @@ namespace MediaBrowser.Controller.MediaEncoding
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture); args += " -ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture);
} }
args += " " + GetAudioFilterParam(state, encodingOptions, false); args += GetAudioFilterParam(state, encodingOptions, false);
return args; return args;
} }

View file

@ -593,6 +593,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
get get
{ {
if (VideoStream == null)
{
return null;
}
if (EncodingHelper.IsCopyCodec(OutputVideoCodec)) if (EncodingHelper.IsCopyCodec(OutputVideoCodec))
{ {
return VideoStream?.Codec; return VideoStream?.Codec;
@ -606,6 +611,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
get get
{ {
if (AudioStream == null)
{
return null;
}
if (EncodingHelper.IsCopyCodec(OutputAudioCodec)) if (EncodingHelper.IsCopyCodec(OutputAudioCodec))
{ {
return AudioStream?.Codec; return AudioStream?.Codec;

View file

@ -234,8 +234,8 @@ namespace MediaBrowser.MediaEncoding.Probing
var channelsValue = channels.Value; var channelsValue = channels.Value;
if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase) || if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase)
string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase)) || string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
{ {
if (channelsValue <= 2) 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; return null;
} }
@ -774,6 +802,35 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.BitRate = bitrate; 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; var disposition = streamInfo.Disposition;
if (disposition != null) 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) private void SetSize(InternalMediaInfoResult data, MediaInfo info)
{ {
if (data.Format != null) if (data.Format != null)

View file

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

View file

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

View file

@ -872,11 +872,34 @@ namespace MediaBrowser.Model.Dlna
return playlistItem; 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; return 192000;
@ -897,14 +920,27 @@ namespace MediaBrowser.Model.Dlna
} }
else 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 // Reduce the bitrate if we're downmixing.
defaultBitrate = targetAudioChannels.Value < 2 ? 128000 : 192000; 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 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. // Seeing webm encoding failures when source has 1 audio channel and 22k bitrate.
@ -938,8 +974,28 @@ namespace MediaBrowser.Model.Dlna
{ {
return 448000; 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( private (PlayMethod?, List<TranscodeReason>) GetVideoDirectPlayProfile(

View file

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