|
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|
|
using System.Globalization;
|
|
|
using System.IO;
|
|
|
using System.Linq;
|
|
|
+using System.Threading;
|
|
|
using MediaBrowser.Controller.Entities;
|
|
|
using MediaBrowser.Model.Configuration;
|
|
|
using MediaBrowser.Model.Dlna;
|
|
@@ -21,6 +22,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|
|
private readonly IMediaEncoder _mediaEncoder;
|
|
|
private readonly IFileSystem _fileSystem;
|
|
|
private readonly ISubtitleEncoder _subtitleEncoder;
|
|
|
+ // private readonly IApplicationPaths _appPaths;
|
|
|
+ // private readonly IAssemblyInfo _assemblyInfo;
|
|
|
|
|
|
public EncodingHelper(IMediaEncoder mediaEncoder, IFileSystem fileSystem, ISubtitleEncoder subtitleEncoder)
|
|
|
{
|
|
@@ -40,56 +43,55 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|
|
{
|
|
|
var hwType = encodingOptions.HardwareAccelerationType;
|
|
|
|
|
|
- if (!encodingOptions.EnableHardwareEncoding)
|
|
|
+ var codecMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
|
{
|
|
|
- hwType = null;
|
|
|
- }
|
|
|
-
|
|
|
- if (string.Equals(hwType, "qsv", StringComparison.OrdinalIgnoreCase) ||
|
|
|
- string.Equals(hwType, "h264_qsv", StringComparison.OrdinalIgnoreCase))
|
|
|
- {
|
|
|
- return GetAvailableEncoder("h264_qsv", defaultEncoder);
|
|
|
- }
|
|
|
+ {"qsv", "h264_qsv"},
|
|
|
+ {"h264_qsv", "h264_qsv"},
|
|
|
+ {"nvenc", "h264_nvenc"},
|
|
|
+ {"amf", "h264_amf"},
|
|
|
+ {"omx", "h264_omx"},
|
|
|
+ {"h264_v4l2m2m", "h264_v4l2m2m"},
|
|
|
+ {"mediacodec", "h264_mediacodec"},
|
|
|
+ {"vaapi", "h264_vaapi"}
|
|
|
+ };
|
|
|
|
|
|
- if (string.Equals(hwType, "nvenc", StringComparison.OrdinalIgnoreCase))
|
|
|
- {
|
|
|
- return GetAvailableEncoder("h264_nvenc", defaultEncoder);
|
|
|
- }
|
|
|
- if (string.Equals(hwType, "amf", StringComparison.OrdinalIgnoreCase))
|
|
|
- {
|
|
|
- return GetAvailableEncoder("h264_amf", defaultEncoder);
|
|
|
- }
|
|
|
- if (string.Equals(hwType, "omx", StringComparison.OrdinalIgnoreCase))
|
|
|
- {
|
|
|
- return GetAvailableEncoder("h264_omx", defaultEncoder);
|
|
|
- }
|
|
|
- if (string.Equals(hwType, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
|
|
|
- {
|
|
|
- return GetAvailableEncoder("h264_v4l2m2m", defaultEncoder);
|
|
|
- }
|
|
|
- if (string.Equals(hwType, "mediacodec", StringComparison.OrdinalIgnoreCase))
|
|
|
- {
|
|
|
- return GetAvailableEncoder("h264_mediacodec", defaultEncoder);
|
|
|
- }
|
|
|
- if (string.Equals(hwType, "vaapi", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(encodingOptions.VaapiDevice))
|
|
|
+ if (!string.IsNullOrEmpty(hwType)
|
|
|
+ && encodingOptions.EnableHardwareEncoding && codecMap.ContainsKey(hwType))
|
|
|
{
|
|
|
- if (IsVaapiSupported(state))
|
|
|
+ if (CheckVaapi(state, hwType, encodingOptions))
|
|
|
{
|
|
|
- return GetAvailableEncoder("h264_vaapi", defaultEncoder);
|
|
|
+ var preferredEncoder = codecMap[hwType];
|
|
|
+
|
|
|
+ if (_mediaEncoder.SupportsEncoder(preferredEncoder))
|
|
|
+ {
|
|
|
+ return preferredEncoder;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
}
|
|
|
|
|
|
+ // Avoid performing a second attempt when the first one
|
|
|
+ // hasn't tried hardware encoding anyway.
|
|
|
+ encodingOptions.EnableHardwareEncoding = false;
|
|
|
return defaultEncoder;
|
|
|
}
|
|
|
|
|
|
- private string GetAvailableEncoder(string preferredEncoder, string defaultEncoder)
|
|
|
+ private bool CheckVaapi(EncodingJobInfo state, string hwType, EncodingOptions encodingOptions)
|
|
|
{
|
|
|
- if (_mediaEncoder.SupportsEncoder(preferredEncoder))
|
|
|
+ if (!string.Equals(hwType, "vaapi", StringComparison.OrdinalIgnoreCase))
|
|
|
{
|
|
|
- return preferredEncoder;
|
|
|
+ // No vaapi requested, return OK.
|
|
|
+ return true;
|
|
|
}
|
|
|
- return defaultEncoder;
|
|
|
+
|
|
|
+ if (string.IsNullOrEmpty(encodingOptions.VaapiDevice))
|
|
|
+ {
|
|
|
+ // No device specified, return OK.
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return IsVaapiSupported(state);
|
|
|
}
|
|
|
|
|
|
private bool IsVaapiSupported(EncodingJobInfo state)
|
|
@@ -340,7 +342,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|
|
|
|
|
public int GetVideoProfileScore(string profile)
|
|
|
{
|
|
|
- string[] list =
|
|
|
+ var list = new[]
|
|
|
{
|
|
|
"ConstrainedBaseline",
|
|
|
"Baseline",
|
|
@@ -538,14 +540,54 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|
|
? string.Empty
|
|
|
: string.Format(",setpts=PTS -{0}/TB", seconds.ToString(_usCulture));
|
|
|
|
|
|
- string fallbackFontParam = string.Empty;
|
|
|
+ // TODO
|
|
|
+ // var fallbackFontPath = Path.Combine(_appPaths.ProgramDataPath, "fonts", "DroidSansFallback.ttf");
|
|
|
+ // string fallbackFontParam = string.Empty;
|
|
|
+
|
|
|
+ // if (!_fileSystem.FileExists(fallbackFontPath))
|
|
|
+ // {
|
|
|
+ // _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(fallbackFontPath));
|
|
|
+ // using (var stream = _assemblyInfo.GetManifestResourceStream(GetType(), GetType().Namespace + ".DroidSansFallback.ttf"))
|
|
|
+ // {
|
|
|
+ // using (var fileStream = _fileSystem.GetFileStream(fallbackFontPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
|
|
|
+ // {
|
|
|
+ // stream.CopyTo(fileStream);
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+
|
|
|
+ // fallbackFontParam = string.Format(":force_style='FontName=Droid Sans Fallback':fontsdir='{0}'", _mediaEncoder.EscapeSubtitleFilterPath(_fileSystem.GetDirectoryName(fallbackFontPath)));
|
|
|
+
|
|
|
+ if (state.SubtitleStream.IsExternal)
|
|
|
+ {
|
|
|
+ var subtitlePath = state.SubtitleStream.Path;
|
|
|
+
|
|
|
+ var charsetParam = string.Empty;
|
|
|
+
|
|
|
+ if (!string.IsNullOrEmpty(state.SubtitleStream.Language))
|
|
|
+ {
|
|
|
+ var charenc = _subtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath, state.SubtitleStream.Language, state.MediaSource.Protocol, CancellationToken.None).Result;
|
|
|
+
|
|
|
+ if (!string.IsNullOrEmpty(charenc))
|
|
|
+ {
|
|
|
+ charsetParam = ":charenc=" + charenc;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // TODO: Perhaps also use original_size=1920x800 ??
|
|
|
+ return string.Format("subtitles=filename='{0}'{1}{2}{3}",
|
|
|
+ _mediaEncoder.EscapeSubtitleFilterPath(subtitlePath),
|
|
|
+ charsetParam,
|
|
|
+ // fallbackFontParam,
|
|
|
+ setPtsParam);
|
|
|
+ }
|
|
|
|
|
|
var mediaPath = state.MediaPath ?? string.Empty;
|
|
|
|
|
|
- return string.Format("subtitles='{0}:si={1}'{2}{3}",
|
|
|
+ return string.Format("subtitles='{0}:si={1}'{2}",
|
|
|
_mediaEncoder.EscapeSubtitleFilterPath(mediaPath),
|
|
|
state.InternalSubtitleStreamOffset.ToString(_usCulture),
|
|
|
- fallbackFontParam,
|
|
|
+ // fallbackFontParam,
|
|
|
setPtsParam);
|
|
|
}
|
|
|
|
|
@@ -1039,51 +1081,72 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|
|
{
|
|
|
var bitrate = request.VideoBitRate;
|
|
|
|
|
|
+ // If specific values were requested, then force the caller to supply a bitrate as well
|
|
|
+ if (request.Height.HasValue && request.Width.HasValue)
|
|
|
+ {
|
|
|
+ return bitrate;
|
|
|
+ }
|
|
|
+
|
|
|
if (videoStream != null)
|
|
|
{
|
|
|
- var isUpscaling = request.Height.HasValue && videoStream.Height.HasValue &&
|
|
|
- request.Height.Value > videoStream.Height.Value && request.Width.HasValue && videoStream.Width.HasValue &&
|
|
|
- request.Width.Value > videoStream.Width.Value;
|
|
|
+ if (bitrate.HasValue && videoStream.BitRate.HasValue)
|
|
|
+ {
|
|
|
+ bitrate = Math.Min(videoStream.BitRate.Value, bitrate.Value);
|
|
|
+ }
|
|
|
|
|
|
- // Don't allow bitrate increases unless upscaling
|
|
|
- if (!isUpscaling)
|
|
|
+ if (bitrate.HasValue)
|
|
|
{
|
|
|
- if (bitrate.HasValue && videoStream.BitRate.HasValue)
|
|
|
+ var inputVideoCodec = videoStream.Codec;
|
|
|
+ bitrate = ScaleBitrate(bitrate.Value, inputVideoCodec, outputVideoCodec);
|
|
|
+
|
|
|
+ // If a max bitrate was requested, don't let the scaled bitrate exceed it
|
|
|
+ if (request.VideoBitRate.HasValue)
|
|
|
{
|
|
|
- bitrate = GetMinBitrate(videoStream.BitRate.Value, bitrate.Value);
|
|
|
+ bitrate = Math.Min(bitrate.Value, request.VideoBitRate.Value);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (bitrate.HasValue)
|
|
|
- {
|
|
|
- var inputVideoCodec = videoStream == null ? null : videoStream.Codec;
|
|
|
- bitrate = ResolutionNormalizer.ScaleBitrate(bitrate.Value, inputVideoCodec, outputVideoCodec);
|
|
|
+ return bitrate;
|
|
|
+ }
|
|
|
|
|
|
- // If a max bitrate was requested, don't let the scaled bitrate exceed it
|
|
|
- if (request.VideoBitRate.HasValue)
|
|
|
- {
|
|
|
- bitrate = Math.Min(bitrate.Value, request.VideoBitRate.Value);
|
|
|
- }
|
|
|
+ private static double GetVideoBitrateScaleFactor(string codec)
|
|
|
+ {
|
|
|
+ if (StringHelper.EqualsIgnoreCase(codec, "h265") ||
|
|
|
+ StringHelper.EqualsIgnoreCase(codec, "hevc") ||
|
|
|
+ StringHelper.EqualsIgnoreCase(codec, "vp9"))
|
|
|
+ {
|
|
|
+ return .5;
|
|
|
}
|
|
|
-
|
|
|
- return bitrate;
|
|
|
+ return 1;
|
|
|
}
|
|
|
|
|
|
- private int GetMinBitrate(int sourceBitrate, int requestedBitrate)
|
|
|
+ private static int ScaleBitrate(int bitrate, string inputVideoCodec, string outputVideoCodec)
|
|
|
{
|
|
|
- if (sourceBitrate <= 2000000)
|
|
|
+ var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec);
|
|
|
+ var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec);
|
|
|
+ var scaleFactor = outputScaleFactor / inputScaleFactor;
|
|
|
+
|
|
|
+ if (bitrate <= 500000)
|
|
|
+ {
|
|
|
+ scaleFactor = Math.Max(scaleFactor, 4);
|
|
|
+ }
|
|
|
+ else if (bitrate <= 1000000)
|
|
|
+ {
|
|
|
+ scaleFactor = Math.Max(scaleFactor, 3);
|
|
|
+ }
|
|
|
+ else if (bitrate <= 2000000)
|
|
|
{
|
|
|
- sourceBitrate = Convert.ToInt32(sourceBitrate * 2.5);
|
|
|
+ scaleFactor = Math.Max(scaleFactor, 2.5);
|
|
|
}
|
|
|
- else if (sourceBitrate <= 3000000)
|
|
|
+ else if (bitrate <= 3000000)
|
|
|
{
|
|
|
- sourceBitrate = Convert.ToInt32(sourceBitrate * 2);
|
|
|
+ scaleFactor = Math.Max(scaleFactor, 2);
|
|
|
}
|
|
|
|
|
|
- var bitrate = Math.Min(sourceBitrate, requestedBitrate);
|
|
|
+ var newBitrate = scaleFactor * bitrate;
|
|
|
|
|
|
- return bitrate;
|
|
|
+ return Convert.ToInt32(newBitrate);
|
|
|
}
|
|
|
|
|
|
public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream)
|
|
@@ -1405,7 +1468,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|
|
videoSizeParam);
|
|
|
}
|
|
|
|
|
|
- private Tuple<int?, int?> GetFixedOutputSize(int? videoWidth,
|
|
|
+ private ValueTuple<int?, int?> GetFixedOutputSize(int? videoWidth,
|
|
|
int? videoHeight,
|
|
|
int? requestedWidth,
|
|
|
int? requestedHeight,
|
|
@@ -1414,11 +1477,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|
|
{
|
|
|
if (!videoWidth.HasValue && !requestedWidth.HasValue)
|
|
|
{
|
|
|
- return new Tuple<int?, int?>(null, null);
|
|
|
+ return new ValueTuple<int?, int?>(null, null);
|
|
|
}
|
|
|
if (!videoHeight.HasValue && !requestedHeight.HasValue)
|
|
|
{
|
|
|
- return new Tuple<int?, int?>(null, null);
|
|
|
+ return new ValueTuple<int?, int?>(null, null);
|
|
|
}
|
|
|
|
|
|
decimal inputWidth = Convert.ToDecimal(videoWidth ?? requestedWidth);
|
|
@@ -1438,7 +1501,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|
|
outputWidth = 2 * Math.Truncate(outputWidth / 2);
|
|
|
outputHeight = 2 * Math.Truncate(outputHeight / 2);
|
|
|
|
|
|
- return new Tuple<int?, int?>(Convert.ToInt32(outputWidth), Convert.ToInt32(outputHeight));
|
|
|
+ return new ValueTuple<int?, int?>(Convert.ToInt32(outputWidth), Convert.ToInt32(outputHeight));
|
|
|
}
|
|
|
|
|
|
public List<string> GetScalingFilters(int? videoWidth,
|
|
@@ -1669,7 +1732,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|
|
var inputHeight = videoStream == null ? null : videoStream.Height;
|
|
|
var threeDFormat = state.MediaSource.Video3DFormat;
|
|
|
|
|
|
- var videoDecoder = GetVideoDecoder(state, options);
|
|
|
+ var videoDecoder = this.GetHardwareAcceleratedVideoDecoder(state, options);
|
|
|
|
|
|
filters.AddRange(GetScalingFilters(inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight));
|
|
|
|
|
@@ -1851,7 +1914,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|
|
inputModifier += " -fflags " + string.Join("", flags.ToArray());
|
|
|
}
|
|
|
|
|
|
- var videoDecoder = GetVideoDecoder(state, encodingOptions);
|
|
|
+ var videoDecoder = this.GetHardwareAcceleratedVideoDecoder(state, encodingOptions);
|
|
|
if (!string.IsNullOrEmpty(videoDecoder))
|
|
|
{
|
|
|
inputModifier += " " + videoDecoder;
|
|
@@ -1893,7 +1956,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|
|
|
|
|
if (state.MediaSource.RequiresLooping)
|
|
|
{
|
|
|
- inputModifier += " -stream_loop -1";
|
|
|
+ inputModifier += " -stream_loop -1 -reconnect_at_eof 1 -reconnect_streamed 1 -reconnect_delay_max 2";
|
|
|
}
|
|
|
|
|
|
return inputModifier;
|
|
@@ -1989,7 +2052,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|
|
{
|
|
|
if (string.IsNullOrEmpty(requestedUrl))
|
|
|
{
|
|
|
- requestedUrl = "test." + videoRequest.OutputContainer;
|
|
|
+ requestedUrl = "test." + videoRequest.Container;
|
|
|
}
|
|
|
|
|
|
videoRequest.VideoCodec = InferVideoCodec(requestedUrl);
|
|
@@ -2015,6 +2078,49 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|
|
}
|
|
|
|
|
|
state.MediaSource = mediaSource;
|
|
|
+
|
|
|
+ var request = state.BaseRequest;
|
|
|
+ if (!string.IsNullOrWhiteSpace(request.AudioCodec))
|
|
|
+ {
|
|
|
+ var supportedAudioCodecsList = request.AudioCodec.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
|
|
+
|
|
|
+ ShiftAudioCodecsIfNeeded(supportedAudioCodecsList, state.AudioStream);
|
|
|
+
|
|
|
+ state.SupportedAudioCodecs = supportedAudioCodecsList.ToArray();
|
|
|
+
|
|
|
+ request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => _mediaEncoder.CanEncodeToAudioCodec(i))
|
|
|
+ ?? state.SupportedAudioCodecs.FirstOrDefault();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ShiftAudioCodecsIfNeeded(List<string> audioCodecs, MediaStream audioStream)
|
|
|
+ {
|
|
|
+ // Nothing to do here
|
|
|
+ if (audioCodecs.Count < 2)
|
|
|
+ {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var inputChannels = audioStream == null ? 6 : audioStream.Channels ?? 6;
|
|
|
+ if (inputChannels >= 6)
|
|
|
+ {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Transcoding to 2ch ac3 almost always causes a playback failure
|
|
|
+ // Keep it in the supported codecs list, but shift it to the end of the list so that if transcoding happens, another codec is used
|
|
|
+ var shiftAudioCodecs = new[] { "ac3", "eac3" };
|
|
|
+ if (audioCodecs.All(i => shiftAudioCodecs.Contains(i, StringComparer.OrdinalIgnoreCase)))
|
|
|
+ {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ while (shiftAudioCodecs.Contains(audioCodecs[0], StringComparer.OrdinalIgnoreCase))
|
|
|
+ {
|
|
|
+ var removed = shiftAudioCodecs[0];
|
|
|
+ audioCodecs.RemoveAt(0);
|
|
|
+ audioCodecs.Add(removed);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private void NormalizeSubtitleEmbed(EncodingJobInfo state)
|
|
@@ -2035,17 +2141,17 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|
|
/// <summary>
|
|
|
/// Gets the name of the output video codec
|
|
|
/// </summary>
|
|
|
- protected string GetVideoDecoder(EncodingJobInfo state, EncodingOptions encodingOptions)
|
|
|
+ protected string GetHardwareAcceleratedVideoDecoder(EncodingJobInfo state, EncodingOptions encodingOptions)
|
|
|
{
|
|
|
if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
|
|
{
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- return GetVideoDecoder(state.MediaSource.VideoType ?? VideoType.VideoFile, state.VideoStream, encodingOptions);
|
|
|
+ return this.GetHardwareAcceleratedVideoDecoder(state.MediaSource.VideoType ?? VideoType.VideoFile, state.VideoStream, encodingOptions);
|
|
|
}
|
|
|
|
|
|
- public string GetVideoDecoder(VideoType videoType, MediaStream videoStream, EncodingOptions encodingOptions)
|
|
|
+ public string GetHardwareAcceleratedVideoDecoder(VideoType videoType, MediaStream videoStream, EncodingOptions encodingOptions)
|
|
|
{
|
|
|
// Only use alternative encoders for video files.
|
|
|
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
|
|
@@ -2070,6 +2176,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|
|
// qsv decoder does not support 10-bit input
|
|
|
if ((videoStream.BitDepth ?? 8) > 8)
|
|
|
{
|
|
|
+ encodingOptions.HardwareDecodingCodecs = Array.Empty<string>();
|
|
|
return null;
|
|
|
}
|
|
|
return "-c:v h264_qsv ";
|
|
@@ -2204,6 +2311,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|
|
|
|
|
else if (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase))
|
|
|
{
|
|
|
+ if (Environment.OSVersion.Platform == PlatformID.Win32NT)
|
|
|
+ {
|
|
|
+ return "-hwaccel dxva2";
|
|
|
+ }
|
|
|
+
|
|
|
switch (videoStream.Codec.ToLower())
|
|
|
{
|
|
|
case "avc":
|
|
@@ -2223,6 +2335,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // Avoid a second attempt if no hardware acceleration is being used
|
|
|
+ encodingOptions.HardwareDecodingCodecs = Array.Empty<string>();
|
|
|
+
|
|
|
// leave blank so ffmpeg will decide
|
|
|
return null;
|
|
|
}
|