fix image subtitle scaling (#949)

* properly scale image-based subtitles for nvidia and software

* fix vaapi image subtitle scaling

* fix qsv image subtitle scaling

* update changelog
This commit is contained in:
Jason Dove
2022-09-04 14:25:05 -05:00
committed by GitHub
parent d669e8114b
commit 9a4f772f53
24 changed files with 233 additions and 45 deletions
+1
View File
@@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `VAAPI`: Downgrade libva from 2.15 to 2.14
- Fix bug with XMLTV that caused some filler to display with primary content details
- Multiple fixes for content scaling with `Nvidia`, `Qsv` and `Vaapi` accelerations
- Properly scale image-based subtitles
### Added
- Add `Preferred Audio Title` feature
@@ -198,11 +198,11 @@ public class TranscodingTests
[ValueSource(typeof(TestData), nameof(TestData.VideoFormats))]
FFmpegProfileVideoFormat profileVideoFormat,
// [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration)
// [ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration)
[ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration)
// [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration)
// [ValueSource(typeof(TestData), nameof(TestData.QsvAcceleration))] HardwareAccelerationKind profileAcceleration)
// [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxAcceleration))] HardwareAccelerationKind profileAcceleration)
[ValueSource(typeof(TestData), nameof(TestData.AmfAcceleration))] HardwareAccelerationKind profileAcceleration)
// [ValueSource(typeof(TestData), nameof(TestData.AmfAcceleration))] HardwareAccelerationKind profileAcceleration)
{
if (inputFormat.Encoder is "mpeg1video" or "msmpeg4v2" or "msmpeg4v3")
{
@@ -239,7 +239,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
hlsPlaylistPath,
hlsSegmentTemplate,
ptsOffset,
playbackSettings.ThreadCount);
playbackSettings.ThreadCount,
128);
_logger.LogDebug("FFmpeg desired state {FrameState}", desiredState);
@@ -363,7 +364,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
hlsPlaylistPath,
hlsSegmentTemplate,
ptsOffset,
Option<int>.None);
Option<int>.None,
128);
var ffmpegSubtitleStream = new ErsatzTV.FFmpeg.MediaStream(0, "ass", StreamKind.Video);
@@ -70,6 +70,7 @@ public class PipelineGeneratorTests
Option<string>.None,
Option<string>.None,
0,
Option<int>.None,
Option<int>.None);
var builder = new PipelineBuilder(
@@ -142,6 +143,7 @@ public class PipelineGeneratorTests
Option<string>.None,
Option<string>.None,
0,
Option<int>.None,
Option<int>.None);
var builder = new PipelineBuilder(
@@ -240,6 +242,7 @@ public class PipelineGeneratorTests
Option<string>.None,
Option<string>.None,
0,
Option<int>.None,
Option<int>.None);
var builder = new PipelineBuilder(
@@ -304,6 +307,7 @@ public class PipelineGeneratorTests
Option<string>.None,
Option<string>.None,
0,
Option<int>.None,
Option<int>.None);
var builder = new PipelineBuilder(
+4 -2
View File
@@ -43,14 +43,16 @@ public static class AvailableEncoders
new EncoderHevcQsv(
currentState,
maybeWatermarkInputFile,
maybeSubtitleInputFile),
maybeSubtitleInputFile,
ffmpegState.QsvExtraHardwareFrames),
(HardwareAccelerationMode.Qsv, VideoFormat.H264) when hardwareCapabilities.CanEncode(
VideoFormat.H264,
desiredState.PixelFormat) =>
new EncoderH264Qsv(
currentState,
maybeWatermarkInputFile,
maybeSubtitleInputFile),
maybeSubtitleInputFile,
ffmpegState.QsvExtraHardwareFrames),
(HardwareAccelerationMode.Vaapi, VideoFormat.Hevc) when hardwareCapabilities.CanEncode(
VideoFormat.Hevc,
@@ -6,21 +6,26 @@ public class EncoderH264Qsv : EncoderBase
{
private readonly FrameState _currentState;
private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile;
private readonly int _extraHardwareFrames;
private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile;
public EncoderH264Qsv(
FrameState currentState,
Option<WatermarkInputFile> maybeWatermarkInputFile,
Option<SubtitleInputFile> maybeSubtitleInputFile)
Option<SubtitleInputFile> maybeSubtitleInputFile,
int extraHardwareFrames)
{
_currentState = currentState;
_maybeWatermarkInputFile = maybeWatermarkInputFile;
_maybeSubtitleInputFile = maybeSubtitleInputFile;
_extraHardwareFrames = extraHardwareFrames;
}
public override string Name => "h264_qsv";
public override StreamKind Kind => StreamKind.Video;
public override IList<string> OutputOptions => new[] { "-c:v", "h264_qsv", "-low_power", "0" };
public override IList<string> OutputOptions =>
new[] { "-c:v", "h264_qsv", "-low_power", "0", "-look_ahead", "0" };
// need to upload if we're still in software and a watermark is used
public override string Filter
@@ -37,11 +42,11 @@ public class EncoderH264Qsv : EncoderBase
// pixel format should already be converted to a supported format by QsvHardwareAccelerationOption
foreach (IPixelFormat pixelFormat in _currentState.PixelFormat)
{
return $"format={pixelFormat.FFmpegName},hwupload=extra_hw_frames=64";
return $"format={pixelFormat.FFmpegName},hwupload=extra_hw_frames={_extraHardwareFrames}";
}
// default to nv12
return "format=nv12,hwupload=extra_hw_frames=64";
return $"format=nv12,hwupload=extra_hw_frames={_extraHardwareFrames}";
}
}
@@ -6,21 +6,26 @@ public class EncoderHevcQsv : EncoderBase
{
private readonly FrameState _currentState;
private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile;
private readonly int _extraHardwareFrames;
private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile;
public EncoderHevcQsv(
FrameState currentState,
Option<WatermarkInputFile> maybeWatermarkInputFile,
Option<SubtitleInputFile> maybeSubtitleInputFile)
Option<SubtitleInputFile> maybeSubtitleInputFile,
int extraHardwareFrames)
{
_currentState = currentState;
_maybeWatermarkInputFile = maybeWatermarkInputFile;
_maybeSubtitleInputFile = maybeSubtitleInputFile;
_extraHardwareFrames = extraHardwareFrames;
}
public override string Name => "hevc_qsv";
public override StreamKind Kind => StreamKind.Video;
public override IList<string> OutputOptions => new[] { "-c:v", "hevc_qsv", "-low_power", "0" };
public override IList<string> OutputOptions =>
new[] { "-c:v", "hevc_qsv", "-low_power", "0", "-look_head", "0" };
// need to upload if we're still in software and a watermark is used
public override string Filter
@@ -37,11 +42,11 @@ public class EncoderHevcQsv : EncoderBase
// pixel format should already be converted to a supported format by QsvHardwareAccelerationOption
foreach (IPixelFormat pixelFormat in _currentState.PixelFormat)
{
return $"format={pixelFormat.FFmpegName},hwupload=extra_hw_frames=64";
return $"format={pixelFormat.FFmpegName},hwupload=extra_hw_frames={_extraHardwareFrames}";
}
// default to nv12
return "format=nv12,hwupload=extra_hw_frames=64";
return $"format=nv12,hwupload=extra_hw_frames={_extraHardwareFrames}";
}
}
+5 -1
View File
@@ -18,8 +18,11 @@ public record FFmpegState(
Option<string> HlsPlaylistPath,
Option<string> HlsSegmentTemplate,
long PtsOffset,
Option<int> ThreadCount)
Option<int> ThreadCount,
Option<int> MaybeQsvExtraHardwareFrames)
{
public int QsvExtraHardwareFrames => MaybeQsvExtraHardwareFrames.IfNone(64);
public static FFmpegState Concat(bool saveReport, string channelName) =>
new(
saveReport,
@@ -37,5 +40,6 @@ public record FFmpegState(
Option<string>.None,
Option<string>.None,
0,
Option<int>.None,
Option<int>.None);
}
@@ -12,13 +12,14 @@ public static class AvailableScaleFilters
HardwareAccelerationMode accelMode,
FrameState currentState,
FrameSize scaledSize,
FrameSize paddedSize) =>
FrameSize paddedSize,
int extraHardwareFrames) =>
accelMode switch
{
HardwareAccelerationMode.Nvenc => new ScaleCudaFilter(currentState, scaledSize, paddedSize),
HardwareAccelerationMode.Qsv when currentState.FrameDataLocation == FrameDataLocation.Hardware ||
scaledSize == paddedSize =>
new ScaleQsvFilter(runtimeInfo, currentState, scaledSize, paddedSize),
new ScaleQsvFilter(runtimeInfo, currentState, scaledSize, paddedSize, extraHardwareFrames),
HardwareAccelerationMode.Vaapi => new ScaleVaapiFilter(currentState, scaledSize, paddedSize),
_ => new ScaleFilter(currentState, scaledSize, paddedSize)
};
@@ -0,0 +1,24 @@
using ErsatzTV.FFmpeg.Filter.Cuda;
using ErsatzTV.FFmpeg.Filter.Qsv;
namespace ErsatzTV.FFmpeg.Filter;
public static class AvailableSubtitleScaleFilters
{
public static IPipelineFilterStep ForAcceleration(
HardwareAccelerationMode accelMode,
FrameState currentState,
FrameSize scaledSize,
FrameSize paddedSize,
int extraHardwareFrames) =>
accelMode switch
{
HardwareAccelerationMode.Nvenc => new SubtitleScaleNppFilter(currentState, scaledSize, paddedSize),
HardwareAccelerationMode.Qsv => new SubtitleScaleQsvFilter(
currentState,
scaledSize,
paddedSize,
extraHardwareFrames),
_ => new ScaleImageFilter(paddedSize)
};
}
@@ -0,0 +1,40 @@
using ErsatzTV.FFmpeg.Format;
namespace ErsatzTV.FFmpeg.Filter.Cuda;
public class SubtitleScaleNppFilter : BaseFilter
{
private readonly FrameState _currentState;
private readonly FrameSize _paddedSize;
private readonly FrameSize _scaledSize;
public SubtitleScaleNppFilter(FrameState currentState, FrameSize scaledSize, FrameSize paddedSize)
{
_currentState = currentState;
_scaledSize = scaledSize;
_paddedSize = paddedSize;
}
public override string Filter
{
get
{
string scale = string.Empty;
if (_currentState.ScaledSize != _scaledSize)
{
string targetSize = $"{_paddedSize.Width}:{_paddedSize.Height}";
string format = string.Empty;
foreach (IPixelFormat pixelFormat in _currentState.PixelFormat)
{
format = $":format={pixelFormat.FFmpegName}";
}
scale = $"scale_npp={targetSize}{format}";
}
return scale;
}
}
public override FrameState NextState(FrameState currentState) => currentState;
}
@@ -4,13 +4,16 @@ public class HardwareUploadFilter : BaseFilter
{
private readonly FFmpegState _ffmpegState;
public HardwareUploadFilter(FFmpegState ffmpegState) => _ffmpegState = ffmpegState;
public HardwareUploadFilter(FFmpegState ffmpegState)
{
_ffmpegState = ffmpegState;
}
public override string Filter => _ffmpegState.EncoderHardwareAccelerationMode switch
{
HardwareAccelerationMode.None => string.Empty,
HardwareAccelerationMode.Nvenc => "hwupload_cuda",
HardwareAccelerationMode.Qsv => "hwupload=extra_hw_frames=64",
HardwareAccelerationMode.Qsv => $"hwupload=extra_hw_frames={_ffmpegState.QsvExtraHardwareFrames}",
HardwareAccelerationMode.Vaapi => "format=nv12|vaapi,hwupload",
_ => "hwupload"
};
@@ -5,12 +5,17 @@ namespace ErsatzTV.FFmpeg.Filter.Qsv;
public class DeinterlaceQsvFilter : BaseFilter
{
private readonly FrameState _currentState;
private readonly int _extraHardwareFrames;
public DeinterlaceQsvFilter(FrameState currentState) => _currentState = currentState;
public DeinterlaceQsvFilter(FrameState currentState, int extraHardwareFrames)
{
_currentState = currentState;
_extraHardwareFrames = extraHardwareFrames;
}
// deinterlace_qsv seems to only support nv12, not p010le
public override string Filter => _currentState.FrameDataLocation == FrameDataLocation.Software
? "format=nv12,hwupload=extra_hw_frames=64,deinterlace_qsv"
? $"format=nv12,hwupload=extra_hw_frames={_extraHardwareFrames},deinterlace_qsv"
: "deinterlace_qsv";
public override FrameState NextState(FrameState currentState)
+10 -3
View File
@@ -10,13 +10,20 @@ public class ScaleQsvFilter : BaseFilter
private readonly FrameState _currentState;
private readonly FrameSize _scaledSize;
private readonly FrameSize _paddedSize;
private readonly int _extraHardwareFrames;
public ScaleQsvFilter(IRuntimeInfo runtimeInfo, FrameState currentState, FrameSize scaledSize, FrameSize paddedSize)
public ScaleQsvFilter(
IRuntimeInfo runtimeInfo,
FrameState currentState,
FrameSize scaledSize,
FrameSize paddedSize,
int extraHardwareFrames)
{
_runtimeInfo = runtimeInfo;
_currentState = currentState;
_scaledSize = scaledSize;
_paddedSize = paddedSize;
_extraHardwareFrames = extraHardwareFrames;
}
public override string Filter
@@ -57,10 +64,10 @@ public class ScaleQsvFilter : BaseFilter
string initialPixelFormat = _currentState.PixelFormat.Match(pf => pf.FFmpegName, FFmpegFormat.NV12);
if (!string.IsNullOrWhiteSpace(scale))
{
return $"format={initialPixelFormat},hwupload=extra_hw_frames=64,{scale}";
return $"format={initialPixelFormat},hwupload=extra_hw_frames={_extraHardwareFrames},{scale}";
}
return $"format={initialPixelFormat},hwupload=extra_hw_frames=64";
return $"format={initialPixelFormat},hwupload=extra_hw_frames={_extraHardwareFrames}";
}
}
@@ -0,0 +1,48 @@
using ErsatzTV.FFmpeg.Format;
namespace ErsatzTV.FFmpeg.Filter.Qsv;
public class SubtitleScaleQsvFilter : BaseFilter
{
private readonly FrameState _currentState;
private readonly FrameSize _scaledSize;
private readonly FrameSize _paddedSize;
private readonly int _extraHardwareFrames;
public SubtitleScaleQsvFilter(
FrameState currentState,
FrameSize scaledSize,
FrameSize paddedSize,
int extraHardwareFrames)
{
_currentState = currentState;
_scaledSize = scaledSize;
_paddedSize = paddedSize;
_extraHardwareFrames = extraHardwareFrames;
}
public override string Filter
{
get
{
string scale = string.Empty;
if (_currentState.ScaledSize != _scaledSize)
{
string targetSize = $"w={_paddedSize.Width}:h={_paddedSize.Height}";
// use software scaling
scale = $"scale={targetSize}";
}
string initialPixelFormat = _currentState.PixelFormat.Match(pf => pf.FFmpegName, FFmpegFormat.NV12);
if (!string.IsNullOrWhiteSpace(scale))
{
return $"{scale},format={initialPixelFormat},hwupload=extra_hw_frames={_extraHardwareFrames}";
}
return $"format={initialPixelFormat},hwupload=extra_hw_frames={_extraHardwareFrames}";
}
}
public override FrameState NextState(FrameState currentState) => currentState;
}
@@ -16,7 +16,7 @@ public class SubtitleHardwareUploadFilter : BaseFilter
{
HardwareAccelerationMode.None => string.Empty,
HardwareAccelerationMode.Nvenc => "hwupload_cuda",
HardwareAccelerationMode.Qsv => "hwupload=extra_hw_frames=64",
HardwareAccelerationMode.Qsv => string.Empty,
// leave vaapi in software since we don't (yet) use overlay_vaapi
HardwareAccelerationMode.Vaapi when _currentState.FrameDataLocation == FrameDataLocation.Software =>
@@ -6,20 +6,14 @@ public class SubtitlePixelFormatFilter : BaseFilter
public SubtitlePixelFormatFilter(FFmpegState ffmpegState) => _ffmpegState = ffmpegState;
public override string Filter
{
get
{
Option<string> maybeFormat = _ffmpegState.EncoderHardwareAccelerationMode switch
{
HardwareAccelerationMode.Nvenc => "yuva420p",
HardwareAccelerationMode.Qsv => "yuva420p",
_ => None
};
public override string Filter => MaybeFormat.Match(f => $"format={f}", () => string.Empty);
return maybeFormat.Match(f => $"format={f}", () => string.Empty);
}
}
public Option<string> MaybeFormat =>_ffmpegState.EncoderHardwareAccelerationMode switch
{
HardwareAccelerationMode.Nvenc => "yuva420p",
HardwareAccelerationMode.Qsv => "yuva420p",
_ => None
};
public override FrameState NextState(FrameState currentState) => currentState;
}
@@ -15,7 +15,7 @@ public class WatermarkHardwareUploadFilter : BaseFilter
{
HardwareAccelerationMode.None => string.Empty,
HardwareAccelerationMode.Nvenc => "hwupload_cuda",
HardwareAccelerationMode.Qsv => "hwupload=extra_hw_frames=64",
HardwareAccelerationMode.Qsv => $"hwupload=extra_hw_frames={_ffmpegState.QsvExtraHardwareFrames}",
// leave vaapi in software since we don't (yet) use overlay_vaapi
HardwareAccelerationMode.Vaapi when _currentState.FrameDataLocation == FrameDataLocation.Software =>
@@ -11,6 +11,7 @@ public static class AvailablePixelFormats
PixelFormat.YUV420P10LE => new PixelFormatYuv420P10Le(),
PixelFormat.YUVJ420P => new PixelFormatYuvJ420P(),
PixelFormat.YUV444P => new PixelFormatYuv444P(),
PixelFormat.YUVA420P => new PixelFormatYuva420P(),
_ => LogUnknownPixelFormat(pixelFormat, logger)
};
+1
View File
@@ -4,6 +4,7 @@ public class FFmpegFormat
{
public const string YUV420P = "yuv420p";
public const string YUV444P = "yuv444p";
public const string YUVA420P = "yuva420p";
public const string P010LE = "p010le";
public const string NV12 = "nv12";
}
+1
View File
@@ -4,6 +4,7 @@ public static class PixelFormat
{
public const string YUV420P = "yuv420p";
public const string YUV420P10LE = "yuv420p10le";
public const string YUVA420P = "yuva420p";
public const string YUVJ420P = "yuvj420p";
public const string YUV444P = "yuv444p";
public const string YUV444P10LE = "yuv444p10le";
@@ -0,0 +1,8 @@
namespace ErsatzTV.FFmpeg.Format;
public class PixelFormatYuva420P : IPixelFormat
{
public string Name => PixelFormat.YUVA420P;
public string FFmpegName => FFmpegFormat.YUVA420P;
public int BitDepth => 8;
}
+37 -6
View File
@@ -400,7 +400,8 @@ public class PipelineBuilder
ffmpegState.EncoderHardwareAccelerationMode,
currentState,
desiredState.ScaledSize,
desiredState.PaddedSize);
desiredState.PaddedSize,
ffmpegState.QsvExtraHardwareFrames);
currentState = scaleFilter.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(scaleFilter));
@@ -427,7 +428,8 @@ public class PipelineBuilder
ffmpegState.EncoderHardwareAccelerationMode,
currentState,
desiredState.ScaledSize,
desiredState.PaddedSize);
desiredState.PaddedSize,
ffmpegState.QsvExtraHardwareFrames);
currentState = scaleFilter.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(scaleFilter));
@@ -482,7 +484,8 @@ public class PipelineBuilder
ffmpegState.EncoderHardwareAccelerationMode,
currentState,
desiredState.ScaledSize,
desiredState.PaddedSize);
desiredState.PaddedSize,
ffmpegState.QsvExtraHardwareFrames);
currentState = scaleFilter.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(scaleFilter));
}
@@ -512,7 +515,8 @@ public class PipelineBuilder
ffmpegState.EncoderHardwareAccelerationMode,
currentState,
desiredState.ScaledSize,
desiredState.PaddedSize);
desiredState.PaddedSize,
ffmpegState.QsvExtraHardwareFrames);
currentState = scaleFilter.NextState(currentState);
videoInputFile.FilterSteps.Add(scaleFilter);
}
@@ -619,9 +623,35 @@ public class PipelineBuilder
_videoInputFile.Iter(f => f.FilterSteps.Add(downloadFilter));
}
subtitleInputFile.FilterSteps.Add(new SubtitlePixelFormatFilter(ffmpegState));
var pixelFormatFilter = new SubtitlePixelFormatFilter(ffmpegState);
subtitleInputFile.FilterSteps.Add(pixelFormatFilter);
subtitleInputFile.FilterSteps.Add(new SubtitleHardwareUploadFilter(currentState, ffmpegState));
FrameState fakeState = currentState;
foreach (string format in pixelFormatFilter.MaybeFormat)
{
fakeState = fakeState with
{
PixelFormat = AvailablePixelFormats.ForPixelFormat(format, _logger)
};
}
// hacky check for actual scaling or padding
if (_videoInputFile.Exists(
v => v.FilterSteps.Any(s => s.Filter.Contains(currentState.PaddedSize.Height.ToString()))))
{
// enable scaling the subtitle stream
fakeState = fakeState with { ScaledSize = new FrameSize(1, 1) };
}
IPipelineFilterStep scaleFilter = AvailableSubtitleScaleFilters.ForAcceleration(
ffmpegState.EncoderHardwareAccelerationMode,
fakeState,
desiredState.ScaledSize,
desiredState.PaddedSize,
ffmpegState.QsvExtraHardwareFrames);
subtitleInputFile.FilterSteps.Add(scaleFilter);
}
else
{
@@ -639,7 +669,8 @@ public class PipelineBuilder
ffmpegState.EncoderHardwareAccelerationMode,
currentState,
desiredState.ScaledSize,
desiredState.PaddedSize);
desiredState.PaddedSize,
ffmpegState.QsvExtraHardwareFrames);
currentState = scaleFilter.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(scaleFilter));
}
+1
View File
@@ -70,4 +70,5 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=videotoolbox/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=xmltv/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=yadif/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=YUVA/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=YUVJ/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>