mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-09-07 03:48:06 +02:00
Merge remote-tracking branch 'upstream/master' into transcode-file-task-schedule
This commit is contained in:
commit
7e7d027bb0
21 changed files with 1228 additions and 365 deletions
|
@ -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"
|
||||||
|
|
|
@ -113,5 +113,10 @@
|
||||||
"TaskCleanTranscode": "Καθαρισμός Kαταλόγου Διακωδικοποιητή",
|
"TaskCleanTranscode": "Καθαρισμός Kαταλόγου Διακωδικοποιητή",
|
||||||
"TaskUpdatePluginsDescription": "Κατεβάζει και εγκαθιστά ενημερώσεις για τις προσθήκες που έχουν ρυθμιστεί για αυτόματη ενημέρωση.",
|
"TaskUpdatePluginsDescription": "Κατεβάζει και εγκαθιστά ενημερώσεις για τις προσθήκες που έχουν ρυθμιστεί για αυτόματη ενημέρωση.",
|
||||||
"TaskUpdatePlugins": "Ενημέρωση Προσθηκών",
|
"TaskUpdatePlugins": "Ενημέρωση Προσθηκών",
|
||||||
"TaskRefreshPeopleDescription": "Ενημερώνει μεταδεδομένα για ηθοποιούς και σκηνοθέτες στην βιβλιοθήκη των πολυμέσων σας."
|
"TaskRefreshPeopleDescription": "Ενημερώνει μεταδεδομένα για ηθοποιούς και σκηνοθέτες στην βιβλιοθήκη των πολυμέσων σας.",
|
||||||
|
"TaskCleanActivityLogDescription": "Διαγράφει καταχωρήσεις απο το αρχείο καταγραφής δραστηριοτήτων παλαιότερες από την ηλικία που έχει διαμορφωθεί.",
|
||||||
|
"TaskCleanActivityLog": "Καθαρό Αρχείο Καταγραφής Δραστηριοτήτων",
|
||||||
|
"Undefined": "Απροσδιόριστο",
|
||||||
|
"Forced": "Εξαναγκασμένο",
|
||||||
|
"Default": "Προεπιλογή"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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" };
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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))
|
||||||
|
|
Loading…
Reference in a new issue