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.
61 lines
2.8 KiB
C#
61 lines
2.8 KiB
C#
namespace ytLive.Services.Encoder;
|
||
|
||
/// <summary>
|
||
/// Everything the FFmpeg subprocess encoder needs for one go-live (TASK 4 ship
|
||
/// step 3). <see cref="RtmpUrl"/> is the FULL ingestion URL — the reusable
|
||
/// stream's ingest address plus its stream key (<c>rtmp://a.rtmp.youtube.com/live2/<key></c>).
|
||
/// Resolution/FPS/bitrate come from the active quality tier; audio (TASK 9)
|
||
/// streams from the mixer through the named pipe <see cref="AudioPipeName"/>.
|
||
/// </summary>
|
||
public sealed class EncoderOptions
|
||
{
|
||
/// <summary>The audio pipe the mixer streams into; the args reference the
|
||
/// same name for ffmpeg's <c>-i</c>.</summary>
|
||
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;
|
||
|
||
/// <summary>Push to <see cref="RtmpUrl"/> (the ON-AIR side). Defaults on so
|
||
/// the pre-recording callers behave unchanged; the VM sets it from the ON-AIR
|
||
/// pill.</summary>
|
||
public bool StreamEnabled { get; init; } = true;
|
||
|
||
/// <summary>Write a local file at <see cref="RecordPath"/> (the REC side).
|
||
/// False unless the REC pill is on.</summary>
|
||
public bool RecordEnabled { get; init; }
|
||
|
||
/// <summary>Local MP4 output path for recording. Required when
|
||
/// <see cref="RecordEnabled"/> is true.</summary>
|
||
public string? RecordPath { get; init; }
|
||
|
||
/// <summary>H.264 encoder name for <c>-c:v</c>; the encoder probes and prefers
|
||
/// nvenc → qsv → amf → libopenh264 when not forced.</summary>
|
||
public string? VideoEncoder { get; init; }
|
||
|
||
public int AudioSampleRate { get; init; } = 48000;
|
||
public int AudioChannels { get; init; } = 2;
|
||
|
||
/// <summary>Name of the mixer's audio pipe (default <see cref="DefaultAudioPipeName"/>).</summary>
|
||
public string AudioPipeName { get; init; } = DefaultAudioPipeName;
|
||
|
||
/// <summary>TASK 65: when set, the encoder muxes a SECOND video output into this ring (fragmented
|
||
/// MP4 on stdout) for the replay buffer. Null = clips off, and the ffmpeg command line is exactly
|
||
/// what it was before the feature existed.</summary>
|
||
public Services.ClipRing? ClipBuffer { get; init; }
|
||
|
||
/// <summary>GOP in frames = Fps × 4s — the YouTube keyframe ≤ 4s compliance bound.</summary>
|
||
public int GopSize => Fps * 4;
|
||
}
|