Files
ersatztv/ErsatzTV.FFmpeg/FFmpegState.cs
T
timothy bbd7356f81 fix(529): floor QSV extra hardware frames so an unthrottled read can't exhaust the pool
A stored qsvExtraHardwareFrames of 0 reached FFmpeg as hwupload=extra_hw_frames=0,
leaving the QSV upload pool no headroom for frames in flight through the filter graph.
Any input that is not throttled then exhausts it: the graph fails with -12 (Cannot
allocate memory), h264_qsv reports "Could not open encoder before EOF", and zero
segments are written.

Measured against the deployed FFmpeg 8.1.2, one real logged command, only the marked
tokens differing:

  readrate 1.05, no burst   + frames 0  -> exit 0,   14 segments
  readrate 1.05 + burst 2/4/8 + frames 0 -> exit 244, ENOMEM, 0 segments
  no readrate at all        + frames 0  -> exit 244, ENOMEM, 0 segments
  readrate 1.05 + burst 8   + frames 64 -> exit 0,   14 segments
  no readrate at all        + frames 64 -> exit 0,   14 segments

So the defect predates #350's cold-start burst: a work-ahead start takes no -readrate
and was already failing on a profile with 0. The burst removed the throttle on every
realtime session, turning an intermittent failure into a near-deterministic one, which
is how it surfaced. Input throttling was doing load-bearing allocation-bounding work
that nobody had written down -- which is why the FFmpeg-level benchmark in #350 and the
argument-generation tests in #516 were both green and neither could see it.

Fixed at FFmpegState.QsvExtraHardwareFrames, the single point every QSV upload site
reads, so one guard covers HardwareUploadFilter, HardwareUploadQsvFilter,
WatermarkHardwareUploadFilter, ScaleQsvFilter, DeinterlaceQsvFilter and
SubtitleScaleQsvFilter rather than six call sites that can drift apart. Values above
the floor are still honored.

Tests negative-controlled: reverting the floor fails exactly the three sub-minimum
cases, with the build verified succeeded first.

Refs #350, #516, #519.
2026-07-21 15:52:57 +02:00

73 lines
2.7 KiB
C#

using ErsatzTV.FFmpeg.OutputFormat;
namespace ErsatzTV.FFmpeg;
public record FFmpegState(
bool SaveReport,
HardwareAccelerationMode DecoderHardwareAccelerationMode,
HardwareAccelerationMode EncoderHardwareAccelerationMode,
Option<string> VaapiDriver,
Option<string> VaapiDevice,
Option<TimeSpan> Start,
Option<TimeSpan> Finish,
bool DoNotMapMetadata,
Option<string> MetadataServiceProvider,
Option<string> MetadataServiceName,
Option<string> MetadataAudioLanguage,
Option<string> MetadataSubtitleLanguage,
Option<string> MetadataSubtitleTitle,
OutputFormatKind OutputFormat,
Option<string> HlsPlaylistPath,
Option<string> HlsSegmentTemplate,
Option<string> HlsInitTemplate,
Option<string> HlsSegmentOptions,
TimeSpan PtsOffset,
Option<int> ThreadCount,
Option<int> MaybeQsvExtraHardwareFrames,
bool IsSongWithProgress,
bool IsHdrTonemap,
string TonemapAlgorithm,
bool IsTroubleshooting,
bool QsvPreferNativeDecoder = false)
{
// the QSV upload pool needs headroom for the frames in flight through the filter graph.
// extra_hw_frames=0 leaves none, so any input that is not throttled exhausts it: the graph
// fails with -12 (Cannot allocate memory), h264_qsv reports "Could not open encoder before
// EOF", and the output file gets no packets at all. Input throttling was the only thing
// hiding it — a work-ahead start (no -readrate) and #350's cold-start burst both remove that
// throttle, so the channel simply dies. A stored 0 is therefore treated as "no pool
// configured" rather than honored literally (ersatztv#529)
public const int MinimumQsvExtraHardwareFrames = 64;
public int QsvExtraHardwareFrames =>
Math.Max(MaybeQsvExtraHardwareFrames.IfNone(MinimumQsvExtraHardwareFrames), MinimumQsvExtraHardwareFrames);
public static FFmpegState Concat(bool saveReport, string channelName) =>
new(
saveReport,
HardwareAccelerationMode.None,
HardwareAccelerationMode.None,
Option<string>.None,
Option<string>.None,
Option<TimeSpan>.None,
Option<TimeSpan>.None,
true, // do not map metadata
"ErsatzTV",
channelName,
Option<string>.None,
Option<string>.None,
Option<string>.None,
OutputFormatKind.MpegTs,
Option<string>.None,
Option<string>.None,
Option<string>.None,
Option<string>.None,
TimeSpan.Zero,
Option<int>.None,
Option<int>.None,
false,
false,
"linear",
false);
}