namespace ytLive.Services.Encoder; /// /// 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 -re would only add a second, fighting pacer), real /// audio via the named pipe /// (TASK 9 — the mixer streams f32le at 48 kHz stereo into \\.\pipe\<name>; /// 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 ffmpeg.exe [Build(...)]. /// public static class FfmpegArgs { public static IReadOnlyList Build(EncoderOptions options, string videoEncoder) { var args = new List { "-hide_banner", "-loglevel", "info", "-stats", "-stats_period", "0.5", "-f", "rawvideo", "-pix_fmt", "bgra", "-video_size", $"{options.RawWidth}x{options.RawHeight}", "-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()]); // TASK 32·4: FRAGMENTED mp4. A plain MP4 writes its index (moov) at the end, so a // power cut or a killed process leaves the video bytes on disk in a file nothing can // open — the whole recording, not the tail. Fragmented writes the index first and // flushes a playable fragment at every keyframe, so an interrupted file still plays up // to the last fragment. Record block only: the FLV stream output has no index. args.AddRange(["-movflags", "+frag_keyframe+empty_moov+default_base_moof"]); args.AddRange(["-f", "mp4", options.RecordPath!]); } // TASK 65: the replay buffer is a third mux of the same encoded frames. Fragmented MP4 on // stdout (stats already own stderr) — one fragment per second, so a clip can start anywhere // inside the chosen window instead of at the last 4-second keyframe. if (options.ClipBuffer != null) { AddVideoTags(args, options, videoEncoder); args.AddRange(["-c:a", "aac", "-ar", options.AudioSampleRate.ToString(), "-ac", options.AudioChannels.ToString()]); args.AddRange(["-movflags", "+frag_keyframe+empty_moov+default_base_moof"]); args.AddRange(["-frag_duration", "1000000"]); args.AddRange(["-f", "mp4", "pipe:1"]); } return args; } private static void AddVideoTags(List args, EncoderOptions options, string videoEncoder) { 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]); AddRateControl(args, 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"]); } /// /// Constant bitrate, spelled the one way each encoder understands. /// /// Why this is not a shared flag: the -b:v/-maxrate/-bufsize triad is capped VBR, /// not CBR — measured 2026-11-12 on the bundled BtbN build, a static frame at -b:v 2000k /// produced 9 kbps with NVENC's default rate control and 1951 kbps with -rc cbr. /// That is the gap behind an @8 stream that averaged 5.1 Mbps. /// /// Per encoder: NVENC and AMF take an explicit mode. QSV has no -rc and already selects /// CBR when bitrate == maxrate (libavcodec/qsvenc.c, select_rc_mode). OpenH264's /// -rc_mode bitrate made no measurable difference on this build, so it is deliberately not /// passed. YouTube's encoder spec requires CBR (support.google.com/youtube/answer/2853702). /// private static void AddRateControl(List args, string videoEncoder) { if (videoEncoder is "h264_nvenc" or "h264_amf") args.AddRange(["-rc", "cbr"]); } }