Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 11s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 25s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m9s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m41s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m23s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 6m23s
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be>
118 lines
5.5 KiB
C#
118 lines
5.5 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,
|
|
Option<double> MaybeReadRate = default,
|
|
Option<double> 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<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);
|
|
}
|