Files
ersatztv/ErsatzTV.Application/FFmpegProfiles/FFmpegProfileBounds.cs
T
timothyandtimothy ed8b602445
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
feat(735): bound the numeric FFmpeg profile fields with a 422, and expose readrate pacing (#847)
Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be>
2026-08-26 22:05:38 +00:00

80 lines
4.0 KiB
C#

using ErsatzTV.Core;
using ErsatzTV.FFmpeg;
namespace ErsatzTV.Application.FFmpegProfiles;
/// <summary>
/// Write-path bounds for the consequential numeric FFmpeg profile fields.
/// A submitted value outside its documented range is REJECTED, naming the bound, rather than
/// accepted and silently rewritten to something the caller never sent (ersatztv#735). The
/// render-time clamps in <see cref="FFmpegState" /> stay as they are: they cover rows that
/// predate this validation or were written out of band, which is what keeps the fix
/// migration-free.
/// </summary>
internal static class FFmpegProfileBounds
{
internal static Validation<BaseError, Unit> ValidateQsvExtraHardwareFrames(int? requested, int? stored)
{
// a row stored before this validation existed may hold anything, and the SPA sends the whole
// profile back on every edit — so rejecting an UNCHANGED legacy value would make an old
// profile uneditable over a field the operator never touched (and cannot even see unless
// hardware acceleration is QSV). only a NEWLY submitted out-of-range value is rejected;
// FFmpegState.QsvExtraHardwareFrames still floors the legacy one at render time
if (requested is null || requested == stored)
{
return Success<BaseError, Unit>(Unit.Default);
}
return requested < FFmpegState.MinimumQsvExtraHardwareFrames
? BaseError.New(
$"QSV extra hardware frames must be at least {FFmpegState.MinimumQsvExtraHardwareFrames}; " +
$"{requested} leaves the QSV upload pool with too little headroom and the transcode writes nothing at all")
: Success<BaseError, Unit>(Unit.Default);
}
internal static Validation<BaseError, Unit> ValidateReadRate(double? requested)
{
if (requested is null)
{
return Success<BaseError, Unit>(Unit.Default);
}
return requested is < FFmpegState.MinimumReadRate or > FFmpegState.MaximumReadRate
? BaseError.New(
$"Read rate must be between {Format(FFmpegState.MinimumReadRate)} and {Format(FFmpegState.MaximumReadRate)}; " +
"below realtime the channel stalls, and above this the input is no longer meaningfully paced")
: Success<BaseError, Unit>(Unit.Default);
}
internal static Validation<BaseError, Unit> ValidateReadRateCatchup(double? requested, double? requestedReadRate)
{
if (requested is null)
{
return Success<BaseError, Unit>(Unit.Default);
}
if (requested is < FFmpegState.MinimumReadRateCatchup or > FFmpegState.MaximumReadRateCatchup)
{
return BaseError.New(
$"Read rate catchup must be between {Format(FFmpegState.MinimumReadRateCatchup)} and " +
$"{Format(FFmpegState.MaximumReadRateCatchup)}");
}
// catchup is the rate a LAGGING input may read at until it is level again, so a value at or
// below the base rate cannot let it recover: EQUAL is rejected too, because a catchup with
// zero headroom is functionally no catchup while still reading as configured. compared
// against the transcode default rather than the stream-copy one because that is the higher
// of the two: a value that clears it clears both, without this check having to know the
// profile's video format
double effectiveReadRate = requestedReadRate ?? FFmpegState.DefaultReadRate;
return requested <= effectiveReadRate
? BaseError.New(
$"Read rate catchup ({Format(requested.Value)}) must be greater than the read rate " +
$"({Format(effectiveReadRate)}); a lagging input cannot catch up at a rate it is already paced at")
: Success<BaseError, Unit>(Unit.Default);
}
private static string Format(double value) =>
value.ToString("0.0####", System.Globalization.CultureInfo.InvariantCulture);
}