perf(video): move tier scaling to ffmpeg

The frame pump now submits crop-sized BGRA frames; FFmpeg scales them to final tier dimensions. Bundled scaler benchmark: 300 608x1080 to 1080x1920 frames in 0.435s (scaler-only).

Reference: https://ffmpeg.org/ffmpeg-filters.html#scale-1
This commit is contained in:
2026-11-04 10:52:35 -08:00
parent 76e8c22815
commit 6310f50c27
11 changed files with 93 additions and 47 deletions
+8
View File
@@ -14,8 +14,16 @@ public sealed class EncoderOptions
public const string DefaultAudioPipeName = "ytllive_audio";
public string RtmpUrl { get; init; } = string.Empty;
/// <summary>Final encoded output width and height.</summary>
public int Width { get; init; } = 1920;
public int Height { get; init; } = 1080;
/// <summary>Raw BGRA frame size from the compositor. Zero uses the final output size.</summary>
public int InputWidth { get; init; }
public int InputHeight { get; init; }
public int RawWidth => InputWidth > 0 ? InputWidth : Width;
public int RawHeight => InputHeight > 0 ? InputHeight : Height;
public int Fps { get; init; } = 60;
public int BitrateKbps { get; init; } = 8000;
+5 -1
View File
@@ -19,7 +19,7 @@ public static class FfmpegArgs
"-hide_banner", "-loglevel", "info",
"-stats", "-stats_period", "0.5",
"-f", "rawvideo", "-pix_fmt", "bgra",
"-video_size", $"{options.Width}x{options.Height}",
"-video_size", $"{options.RawWidth}x{options.RawHeight}",
"-framerate", options.Fps.ToString(),
"-i", "pipe:0",
"-f", "f32le", "-ar", options.AudioSampleRate.ToString(),
@@ -53,6 +53,10 @@ public static class FfmpegArgs
{
var gop = options.GopSize;
args.AddRange(["-map", "0:v", "-map", "1:a"]);
if (options.RawWidth != options.Width || options.RawHeight != options.Height)
{
args.AddRange(["-vf", $"scale={options.Width}:{options.Height}:flags=bilinear,setsar=1"]);
}
args.AddRange(["-c:v", videoEncoder]);
args.AddRange(["-b:v", $"{options.BitrateKbps}k", "-maxrate", $"{options.BitrateKbps}k",
"-bufsize", $"{options.BitrateKbps * 2}k"]);
+4 -6
View File
@@ -10,7 +10,7 @@ namespace ytLive.Services.Encoder;
/// The live frame producer (TASK 4 ship step 5): the bridge between the capture
/// managers + compositor and the encoder. While live it snapshots the active
/// scene each tick, resolves every element to its latest frame, composites it
/// into the tier's output frame, and paces frames into the encoder at the tier's
/// into the tier's cropped input frame, and paces frames into the encoder at the tier's
/// FPS. All collaborators are constructor-injected seams (scene, resolver,
/// options, encoder factory, pacing delay) so the pump stays free of WPF and of
/// the capture managers and is fully hermetic in tests.
@@ -722,11 +722,9 @@ public sealed class FramePump : IDisposable
byte[]? scratch, Func<SceneElement, VideoFrame?> resolver,
CompositeTimings? timings = null)
{
// 1:1 guard: the cache stores the OUTPUT bytes and scratch is source-sized —
// at 1:1 they are the same buffer size. Off-size tiers (a vertical 1080×1920
// tier, an H264-1080p tier) fall back to a per-tick full render; the deployed
// config is master-sized and caching a per-tick fresh scale buffer is the known
// vertical-tier follow-up (see ai.md), not silently baked here.
// The cache stores the compositor frame at crop size; FFmpeg scales that frame
// to the final output dimensions. With product options, scratch and rendered
// frames are therefore the same size, including for vertical output.
var useCache = scratch != null && options.SourceRectWidth == options.OutputWidth
&& options.SourceRectHeight == options.OutputHeight;
var signature = useCache