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>
80 lines
4.0 KiB
C#
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);
|
|
}
|