using ErsatzTV.FFmpeg.OutputFormat; namespace ErsatzTV.FFmpeg; public record FFmpegState( bool SaveReport, HardwareAccelerationMode DecoderHardwareAccelerationMode, HardwareAccelerationMode EncoderHardwareAccelerationMode, Option VaapiDriver, Option VaapiDevice, Option Start, Option Finish, bool DoNotMapMetadata, Option MetadataServiceProvider, Option MetadataServiceName, Option MetadataAudioLanguage, Option MetadataSubtitleLanguage, Option MetadataSubtitleTitle, OutputFormatKind OutputFormat, Option HlsPlaylistPath, Option HlsSegmentTemplate, Option HlsInitTemplate, Option HlsSegmentOptions, TimeSpan PtsOffset, Option ThreadCount, Option MaybeQsvExtraHardwareFrames, bool IsSongWithProgress, bool IsHdrTonemap, string TonemapAlgorithm, bool IsTroubleshooting, bool QsvPreferNativeDecoder = false, Option MaybeReadRate = default, Option MaybeReadRateCatchup = default) { // 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); // realtime pacing. an unset profile keeps the values these constants name, which are the ones // the pipeline hardcoded before they became configurable (ersatztv#735) public const double DefaultReadRate = 1.05; public const double DefaultStreamCopyReadRate = 1.0; // how fast a LAGGING realtime input may read until it is level again. measured on the #726 // repro (embedded dvd_subtitle -> overlay, QSV encode): 1.05 alone sustains 0.53x, catchup 2.0 // reaches 0.711x, and 6.0 restores the full 1.067x that the same pipeline achieves with no // subtitle at all. 20.0 also measures 1.067x — i.e. the value is not a throughput dial above // the point where the input catches up, so 6.0 is chosen as the smallest measured-sufficient // ceiling rather than the largest that works (ersatztv#726) public const double DefaultReadRateCatchup = 6.0; // below realtime the process reads slower than a live client consumes and the channel stalls; // ersatztv#726 is that failure, measured at an effective 0.53x. the ceiling is a CHOSEN bound, // not a measured cliff: it exists so the field cannot be used to effectively disable pacing, // which is the configuration ersatztv#529 measured to produce zero segments on a QSV pipeline public const double MinimumReadRate = 1.0; public const double MaximumReadRate = 2.0; // catchup is a ceiling that applies only WHILE an input is behind, so it is bounded more // loosely than the base rate; the same chosen-not-measured caveat applies to the ceiling. // the FLOOR is only a write-path bound: at render time the resolved base rate is always at // least MinimumReadRate, so Math.Max below already dominates it public const double MinimumReadRateCatchup = 1.0; public const double MaximumReadRateCatchup = 10.0; // clamped for the same reason QsvExtraHardwareFrames is: a row written out of band (or before // the write path validated the field) must not reach FFmpeg unbounded. the write path rejects // an out-of-range value with a 422 naming the bound, so this is belt-and-braces, not the // primary guard (ersatztv#735) public double ReadRateFor(bool isStreamCopy) => MaybeReadRate.Match( configured => Math.Clamp(configured, MinimumReadRate, MaximumReadRate), () => isStreamCopy ? DefaultStreamCopyReadRate : DefaultReadRate); // a catchup rate below the base rate cannot let a lagging input recover, so the resolved base // rate is its real floor — no separate lower clamp, which would be unreachable behind this Max public double ReadRateCatchupFor(bool isStreamCopy) => Math.Max( Math.Min(MaybeReadRateCatchup.IfNone(DefaultReadRateCatchup), MaximumReadRateCatchup), ReadRateFor(isStreamCopy)); public static FFmpegState Concat(bool saveReport, string channelName) => new( saveReport, HardwareAccelerationMode.None, HardwareAccelerationMode.None, Option.None, Option.None, Option.None, Option.None, true, // do not map metadata "ErsatzTV", channelName, Option.None, Option.None, Option.None, OutputFormatKind.MpegTs, Option.None, Option.None, Option.None, Option.None, TimeSpan.Zero, Option.None, Option.None, false, false, "linear", false); }