aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs')
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs182
1 files changed, 118 insertions, 64 deletions
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 65f6b79656..ff8d84d45e 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -86,6 +86,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0);
private readonly Version _minFFmpegRkmppHevcDecDoviRpu = new Version(7, 1, 1);
private readonly Version _minFFmpegReadrateCatchupOption = new Version(8, 0);
+ private readonly Version _minFFmpegNoiseBsfDrop = new Version(5, 0);
private static readonly string[] _videoProfilesH264 =
[
@@ -1267,16 +1268,13 @@ namespace MediaBrowser.Controller.MediaEncoding
.Append(_mediaEncoder.GetInputPathArgument(state));
}
- // sub2video for external graphical subtitles
- if (state.SubtitleStream is not null
- && ShouldEncodeSubtitle(state)
- && !state.SubtitleStream.IsTextSubtitleStream
- && state.SubtitleStream.IsExternal)
+ if (NeedsExternalSubtitleMuxing(state))
{
var subtitlePath = state.SubtitleStream.Path;
- var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
+ var isGraphicalBurnIn = ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream;
// dvdsub/vobsub graphical subtitles use .sub+.idx pairs
+ var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase))
{
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
@@ -1307,7 +1305,7 @@ namespace MediaBrowser.Controller.MediaEncoding
arg.Append(' ').Append(seekSubParam);
}
- if (!string.IsNullOrEmpty(canvasArgs))
+ if (isGraphicalBurnIn && !string.IsNullOrEmpty(canvasArgs))
{
arg.Append(canvasArgs);
}
@@ -1550,20 +1548,61 @@ namespace MediaBrowser.Controller.MediaEncoding
public string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer)
{
- var bitStreamArgs = string.Empty;
+ var filters = new List<string>();
+
+ var noiseFilter = GetCopiedAudioTrimBsf(state);
+ if (!string.IsNullOrEmpty(noiseFilter))
+ {
+ filters.Add(noiseFilter);
+ }
+
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, "ts", StringComparison.OrdinalIgnoreCase)
|| string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase)
- || string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
+ || string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase))
+ && IsAAC(state.AudioStream))
+ {
+ filters.Add("aac_adtstoasc");
+ }
+
+ return filters.Count == 0
+ ? string.Empty
+ : " -bsf:a " + string.Join(',', filters);
+ }
+
+ // When video is transcoded, accurate_seek (the default) trims video to the
+ // exact seek point via decoder-side frame discard. But stream-copied audio
+ // bypasses the decoder, so it starts from the nearest keyframe — potentially
+ // seconds before the target. Use the noise bsf to drop copied audio packets
+ // before the seek target, achieving the same trim precision without
+ // re-encoding. The noise bsf's drop= parameter requires ffmpeg >= 5.0.
+ // Important: make sure not to use it with wtv because it breaks seeking
+ private string GetCopiedAudioTrimBsf(EncodingJobInfo state)
+ {
+ if (state.TranscodingType is not TranscodingJobType.Hls
+ || !state.IsVideoRequest
+ || IsCopyCodec(state.OutputVideoCodec)
+ || !IsCopyCodec(state.OutputAudioCodec)
+ || string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase)
+ || _mediaEncoder.EncoderVersion < _minFFmpegNoiseBsfDrop)
{
- bitStreamArgs = GetBitStreamArgs(state, MediaStreamType.Audio);
- bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs;
+ return null;
+ }
+
+ var startTicks = state.BaseRequest.StartTimeTicks ?? 0;
+ if (startTicks <= 0)
+ {
+ return null;
}
- return bitStreamArgs;
+ var seekSeconds = startTicks / (double)TimeSpan.TicksPerSecond;
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ "noise=drop='lt(pts*tb\\,{0:F3})'",
+ seekSeconds);
}
public static string GetSegmentFileExtension(string segmentContainer)
@@ -1766,13 +1805,13 @@ namespace MediaBrowser.Controller.MediaEncoding
{
param += encoderPreset switch
{
- EncoderPreset.veryslow => " -preset p7",
- EncoderPreset.slower => " -preset p6",
- EncoderPreset.slow => " -preset p5",
- EncoderPreset.medium => " -preset p4",
- EncoderPreset.fast => " -preset p3",
- EncoderPreset.faster => " -preset p2",
- _ => " -preset p1"
+ EncoderPreset.veryslow => " -preset p7",
+ EncoderPreset.slower => " -preset p6",
+ EncoderPreset.slow => " -preset p5",
+ EncoderPreset.medium => " -preset p4",
+ EncoderPreset.fast => " -preset p3",
+ EncoderPreset.faster => " -preset p2",
+ _ => " -preset p1"
};
}
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf)
@@ -1782,11 +1821,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{
param += encoderPreset switch
{
- EncoderPreset.veryslow => " -quality quality",
- EncoderPreset.slower => " -quality quality",
- EncoderPreset.slow => " -quality quality",
- EncoderPreset.medium => " -quality balanced",
- _ => " -quality speed"
+ EncoderPreset.veryslow => " -quality quality",
+ EncoderPreset.slower => " -quality quality",
+ EncoderPreset.slow => " -quality quality",
+ EncoderPreset.medium => " -quality balanced",
+ _ => " -quality speed"
};
if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
@@ -1806,11 +1845,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{
param += encoderPreset switch
{
- EncoderPreset.veryslow => " -prio_speed 0",
- EncoderPreset.slower => " -prio_speed 0",
- EncoderPreset.slow => " -prio_speed 0",
- EncoderPreset.medium => " -prio_speed 0",
- _ => " -prio_speed 1"
+ EncoderPreset.veryslow => " -prio_speed 0",
+ EncoderPreset.slower => " -prio_speed 0",
+ EncoderPreset.slow => " -prio_speed 0",
+ EncoderPreset.medium => " -prio_speed 0",
+ _ => " -prio_speed 1"
};
}
@@ -2017,11 +2056,15 @@ namespace MediaBrowser.Controller.MediaEncoding
args += keyFrameArg + gopArg;
}
- // global_header produced by AMD HEVC VA-API encoder causes non-playable fMP4 on iOS
+ // The in-band Parameter Sets generated by the AMD HEVC VA-API encoder is inconsistent
+ // with the extradata generated by ffmpeg, causing decoding failures when using hvc1.
if (string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
&& _mediaEncoder.IsVaapiDeviceAmd)
{
- args += " -flags:v -global_header";
+ // Extracting the extradata from the in-band PS to bypass the issue.
+ // This can be removed once the issue is resolved in libva or Mesa.
+ // Transcoding is unavoidable here, so using BSF will not conflict with BSF in remuxing.
+ args += " -flags:v -global_header -bsf:v extract_extradata=remove=0";
}
return args;
@@ -2762,25 +2805,29 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
{
+#pragma warning disable SA1008
return (inputChannels, outputChannels) switch
{
- (>= 6, >= 6 or 0) => Math.Min(640000, bitrate),
- (> 0, > 0) => Math.Min(outputChannels * 128000, bitrate),
- (> 0, _) => Math.Min(inputChannels * 128000, bitrate),
+ ( >= 6, >= 6 or 0) => Math.Min(640000, bitrate),
+ ( > 0, > 0) => Math.Min(outputChannels * 128000, bitrate),
+ ( > 0, _) => Math.Min(inputChannels * 128000, bitrate),
(_, _) => Math.Min(384000, bitrate)
};
+#pragma warning restore SA1008
}
if (string.Equals(audioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "dca", StringComparison.OrdinalIgnoreCase))
{
+#pragma warning disable SA1008
return (inputChannels, outputChannels) switch
{
- (>= 6, >= 6 or 0) => Math.Min(768000, bitrate),
- (> 0, > 0) => Math.Min(outputChannels * 136000, bitrate),
- (> 0, _) => Math.Min(inputChannels * 136000, bitrate),
+ ( >= 6, >= 6 or 0) => Math.Min(768000, bitrate),
+ ( > 0, > 0) => Math.Min(outputChannels * 136000, bitrate),
+ ( > 0, _) => Math.Min(inputChannels * 136000, bitrate),
(_, _) => Math.Min(672000, bitrate)
};
+#pragma warning restore SA1008
}
// Empty bitrate area is not allow on iOS
@@ -3001,23 +3048,6 @@ namespace MediaBrowser.Controller.MediaEncoding
}
seekParam += string.Format(CultureInfo.InvariantCulture, "-ss {0}", _mediaEncoder.GetTimeParameter(seekTick));
-
- if (state.IsVideoRequest)
- {
- // If we are remuxing, then the copied stream cannot be seeked accurately (it will seek to the nearest
- // keyframe). If we are using fMP4, then force all other streams to use the same inaccurate seeking to
- // avoid A/V sync issues which cause playback issues on some devices.
- // When remuxing video, the segment start times correspond to key frames in the source stream, so this
- // option shouldn't change the seeked point that much.
- // Important: make sure not to use it with wtv because it breaks seeking
- if (state.TranscodingType is TranscodingJobType.Hls
- && string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase)
- && (IsCopyCodec(state.OutputVideoCodec) || IsCopyCodec(state.OutputAudioCodec))
- && !string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase))
- {
- seekParam += " -noaccurate_seek";
- }
- }
}
return seekParam;
@@ -3072,11 +3102,8 @@ namespace MediaBrowser.Controller.MediaEncoding
int audioStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.AudioStream);
if (state.AudioStream.IsExternal)
{
- bool hasExternalGraphicsSubs = state.SubtitleStream is not null
- && ShouldEncodeSubtitle(state)
- && state.SubtitleStream.IsExternal
- && !state.SubtitleStream.IsTextSubtitleStream;
- int externalAudioMapIndex = hasExternalGraphicsSubs ? 2 : 1;
+ bool hasExternalSubAsInput = NeedsExternalSubtitleMuxing(state);
+ int externalAudioMapIndex = hasExternalSubAsInput ? 2 : 1;
args += string.Format(
CultureInfo.InvariantCulture,
@@ -3104,12 +3131,31 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (subtitleMethod == SubtitleDeliveryMethod.Embed)
{
- int subtitleStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream);
+ if (state.SubtitleStream.IsExternal)
+ {
+ // External subtitle file is added as second FFmpeg input.
+ // For single-stream files (SRT/ASS/VTT) the in-file index is always 0.
+ // For multi-stream containers (MKS) we count how many streams from
+ // the same file appear before the selected one.
+ var inFileIndex = state.MediaSource.MediaStreams
+ .Where(s => string.Equals(s.Path, state.SubtitleStream.Path, StringComparison.Ordinal))
+ .TakeWhile(s => s.Index != state.SubtitleStream.Index)
+ .Count();
- args += string.Format(
- CultureInfo.InvariantCulture,
- " -map 0:{0}",
- subtitleStreamIndex);
+ args += string.Format(
+ CultureInfo.InvariantCulture,
+ " -map 1:{0}",
+ inFileIndex);
+ }
+ else
+ {
+ int subtitleStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream);
+
+ args += string.Format(
+ CultureInfo.InvariantCulture,
+ " -map 0:{0}",
+ subtitleStreamIndex);
+ }
}
else if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)
{
@@ -7886,6 +7932,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|| (state.BaseRequest.AlwaysBurnInSubtitleWhenTranscoding && !IsCopyCodec(state.OutputVideoCodec));
}
+ private static bool NeedsExternalSubtitleMuxing(EncodingJobInfo state)
+ {
+ return state.SubtitleStream is not null
+ && state.SubtitleStream.IsExternal
+ && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed
+ || (ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream));
+ }
+
public static string GetVideoSyncOption(string videoSync, Version encoderVersion)
{
if (string.IsNullOrEmpty(videoSync))