Slice 2 of TASK 65. 602 green, 0 warnings. **Clips are still off**: no `ClipBuffer` is passed, so the ffmpeg command line is byte-for-byte what it was before this commit, and a test asserts exactly that (the original args are a strict prefix of the new ones). - `FfmpegArgs` appends a fragmented-MP4 output to `pipe:1` only when `ClipBuffer` is set. One-second fragments (`-frag_duration 1000000`) rather than the 4 s GOP, so a clip can start inside the window instead of at the last keyframe. Stats already own stderr, so stdout was free. - `Fmp4FragmentSplitter` turns that byte stream into whole fragments: `ftyp`+`moov` is the init segment, each `moof`+`mdat` is a fragment. It parses box headers itself, survives a box being cut anywhere across reads, and **resyncs instead of throwing** — a clip fault must never take the stream down. - `FfmpegEncoder` reads it on its own task, never blocking the frame path. Any exception logs and disables clips for the session; the broadcast is untouched. - `IEncoderProcess` gained `StandardOutputBytes` (the seam already had the text reader for probing). Next slices: the replay layer + retention, then the Settings switch with disabled-but-remembered controls.
86 lines
4.2 KiB
C#
86 lines
4.2 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\<name></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.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<string> 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]);
|
|
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"]);
|
|
}
|
|
}
|