Files
gramps bd396e488c fix(rec): recordings played ~1.4x fast — CFR emission, drop -re
Root cause (measured, not guessed): the take-3 'rebase on overrun' reset
nextTick to wall-now, erasing every missed slot. Pump delivered 215-219/300
per 5s (~43fps) but rawvideo carries no timestamps — ffmpeg muxes by frame
count at -framerate 60, so every recording played fast with honest-looking
stats. A second pacer, ffmpeg -re, throttled the demux separately
('Resumed reading ... after a lag' 0.79s->4.82s).

Fix follows libobs video-io.c (https://github.com/obsproject/obs-studio)
- the video thread never resets its deadline; one frame per interval slot,
a late render repeats content (judder), never skips time:
1. FramePump: count-based emission, while(now>=nextTick){Submit; nextTick+=I}
2. FfmpegArgs: -re removed - the pump is the pacer

Muxed duration is now frame-count/fps == wall time by construction. Covers
game background + webcam (one shared pump). 0 warnings; suite 288/289 -
the one failure (Composite_FullScene_MasterPixels 1380,700) also fails with
this change stashed: pre-existing, untouched, recorded as follow-up.
2026-09-10 08:35:30 -07:00

63 lines
2.7 KiB
C#

namespace ytLive.Services.Encoder;
/// <summary>
/// Builds the FFmpeg command line for a live RTMP push (TASK 4 ship step 3):
/// raw BGRA frames via stdin (paced by the frame pump — the pump holds a strict
/// CFR cadence, so <c>-re</c> would only add a second, fighting pacer), real
/// audio via the named pipe
/// (TASK 9 — the mixer streams f32le at 48 kHz stereo into <c>\\.\pipe\&lt;name&gt;</c>;
/// WASAPI loopback + the mic ride the pipe instead of the old anullsrc silence),
/// H.264 + AAC encoding, FLV muxing to the ingestion URL. Pure — the encoder
/// just starts <c>ffmpeg.exe [Build(...)]</c>.
/// </summary>
public static class FfmpegArgs
{
public static IReadOnlyList<string> Build(EncoderOptions options, string videoEncoder)
{
var args = new List<string>
{
"-hide_banner", "-loglevel", "info",
"-stats", "-stats_period", "0.5",
"-f", "rawvideo", "-pix_fmt", "bgra",
"-video_size", $"{options.Width}x{options.Height}",
"-framerate", options.Fps.ToString(),
"-i", "pipe:0",
"-f", "f32le", "-ar", options.AudioSampleRate.ToString(),
"-ac", options.AudioChannels.ToString(),
"-i", $@"\\.\pipe\{options.AudioPipeName}",
};
// Each output is a self-contained block: its own -map, codec options,
// format, and destination. Record+stream encodes the video twice off the
// one raw input pipe (cheap on NVENC).
if (options.StreamEnabled)
{
AddVideoTags(args, options, videoEncoder);
args.AddRange(["-c:a", "aac", "-b:a", "128k",
"-ar", options.AudioSampleRate.ToString(), "-ac", options.AudioChannels.ToString()]);
args.AddRange(["-f", "flv", options.RtmpUrl]);
}
if (options.RecordEnabled)
{
AddVideoTags(args, options, videoEncoder);
args.AddRange(["-c:a", "aac",
"-ar", options.AudioSampleRate.ToString(), "-ac", options.AudioChannels.ToString()]);
args.AddRange(["-f", "mp4", options.RecordPath!]);
}
return args;
}
private static void AddVideoTags(List<string> args, EncoderOptions options, string videoEncoder)
{
var gop = options.GopSize;
args.AddRange(["-map", "0:v", "-map", "1:a"]);
args.AddRange(["-c:v", videoEncoder]);
args.AddRange(["-b:v", $"{options.BitrateKbps}k", "-maxrate", $"{options.BitrateKbps}k",
"-bufsize", $"{options.BitrateKbps * 2}k"]);
args.AddRange(["-g", gop.ToString(), "-keyint_min", gop.ToString(),
"-sc_threshold", "0", "-bf", "0", "-pix_fmt", "yuv420p"]);
}
}