Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 7s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 12s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 19s
review-verdict/h10 Review-verdict: MERGEABLE @ 5459e21 (base: main)
PR Gates / decisions lifecycle (pull_request) Successful in 18s
PR Gates / Docs update reminder (pull_request) Successful in 25s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 27s
Review verdict / Set review-verdict status (pull_request_target) Successful in 12s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 5m7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m50s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m9s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 5m50s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 5m11s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 8s
The write path used to accept an out-of-range `qsvExtraHardwareFrames`, store the floored value instead and return 200 — the accepted residual #529 recorded, where a client PUTs 0 and reads back 64. `FFmpegProfileBounds` now rejects a newly submitted out-of-range value with a 422 naming the bound and its consequence; the render-time floor in `FFmpegState` stays, which is what keeps the change migration-free for rows written before this existed. On update an UNCHANGED legacy value is still accepted, because the SPA round-trips the whole profile and rejecting it would make an old profile uneditable over a field the operator never touched and, outside QSV, cannot see. The `-readrate` / `-readrate_catchup` pacing that `PipelineBuilderBase` hardcoded becomes two nullable profile fields with the same bounded treatment. During #726 the only way to try a different pacing value was to rebuild the app and replay command lines by hand on the Docker host. `null` means unset and resolves to the values the pipeline used before, so an untouched profile paces identically; catchup stays on by default and capability-gated in code. The draft helpers moved out of `FFmpegProfilesScreen.tsx` into `ffmpegProfileDraft.ts` so the bounds can be tested directly — a component module that exports non-components trips react-refresh, and the baseline was warning-free. Mutation-checked: ignoring the pacing override, disarming each bound, and removing the unchanged-legacy exemption each redden their own tests. fixes #735 Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
91 lines
3.0 KiB
C#
91 lines
3.0 KiB
C#
using System.ComponentModel;
|
|
using ErsatzTV.Application.FFmpegProfiles;
|
|
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Core.FFmpeg;
|
|
|
|
namespace ErsatzTV.Controllers.Api.Requests;
|
|
|
|
public record CreateFFmpegProfileRequest(
|
|
string Name,
|
|
int ThreadCount,
|
|
bool NormalizeAudio,
|
|
bool NormalizeVideo,
|
|
HardwareAccelerationKind HardwareAcceleration,
|
|
string VaapiDisplay,
|
|
VaapiDriver VaapiDriver,
|
|
string VaapiDevice,
|
|
[property: Description(
|
|
"Extra surfaces in the QSV upload pool. Must be at least 64 when set; a smaller pool "
|
|
+ "leaves no headroom for frames in flight and the transcode writes nothing at all. On update, a "
|
|
+ "value equal to the one already stored is accepted unchanged, so a profile written before this "
|
|
+ "validation existed stays editable.")]
|
|
int? QsvExtraHardwareFrames,
|
|
int ResolutionId,
|
|
ScalingBehavior ScalingBehavior,
|
|
FilterMode PadMode,
|
|
FFmpegProfileVideoFormat VideoFormat,
|
|
string VideoProfile,
|
|
string VideoPreset,
|
|
bool AllowBFrames,
|
|
FFmpegProfileBitDepth BitDepth,
|
|
int VideoBitrate,
|
|
int VideoBufferSize,
|
|
FFmpegProfileTonemapAlgorithm TonemapAlgorithm,
|
|
FFmpegProfileAudioFormat AudioFormat,
|
|
int AudioBitrate,
|
|
int AudioBufferSize,
|
|
NormalizeLoudnessMode NormalizeLoudnessMode,
|
|
double? TargetLoudness,
|
|
int AudioChannels,
|
|
int AudioSampleRate,
|
|
bool NormalizeFramerate,
|
|
bool NormalizeColors,
|
|
bool DeinterlaceVideo,
|
|
bool? QsvPreferNativeDecoder = null,
|
|
[property: Description(
|
|
"Realtime pacing multiplier for the input. Unset keeps the built-in pacing "
|
|
+ "(1.05, or 1.0 for a stream copy). Must be between 1.0 and 2.0 when set.")]
|
|
double? ReadRate = null,
|
|
[property: Description(
|
|
"Rate a lagging realtime input may read at until it is level again. Unset keeps the "
|
|
+ "built-in 6.0. Must be between 1.0 and 10.0, and GREATER than the read rate — equal is "
|
|
+ "zero headroom, which is functionally no catchup.")]
|
|
double? ReadRateCatchup = null)
|
|
{
|
|
public CreateFFmpegProfile ToCommand() =>
|
|
new(
|
|
Name,
|
|
ThreadCount,
|
|
NormalizeAudio,
|
|
NormalizeVideo,
|
|
HardwareAcceleration,
|
|
VaapiDisplay,
|
|
VaapiDriver,
|
|
VaapiDevice,
|
|
QsvExtraHardwareFrames,
|
|
ResolutionId,
|
|
ScalingBehavior,
|
|
PadMode,
|
|
VideoFormat,
|
|
VideoProfile,
|
|
VideoPreset,
|
|
AllowBFrames,
|
|
BitDepth,
|
|
VideoBitrate,
|
|
VideoBufferSize,
|
|
TonemapAlgorithm,
|
|
AudioFormat,
|
|
AudioBitrate,
|
|
AudioBufferSize,
|
|
NormalizeLoudnessMode,
|
|
TargetLoudness,
|
|
AudioChannels,
|
|
AudioSampleRate,
|
|
NormalizeFramerate,
|
|
NormalizeColors,
|
|
DeinterlaceVideo,
|
|
QsvPreferNativeDecoder ?? true,
|
|
ReadRate,
|
|
ReadRateCatchup);
|
|
}
|