feat(65): the clip mux — a third output on the same ffmpeg, drained off the frame path
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.
This commit is contained in:
@@ -118,6 +118,11 @@ public sealed class FfmpegEncoder : IFfmpegEncoder
|
||||
_stopRequested = false;
|
||||
_stderrLoop = RunStderrLoopAsync(process);
|
||||
StartDrainLoop(process);
|
||||
|
||||
// TASK 65: the clip mux is a side channel. It reads its own thread, never blocks the frame
|
||||
// path, and a fault disables clips instead of touching the stream.
|
||||
if (options.ClipBuffer is { } clipRing)
|
||||
_ = Task.Run(() => ReadClipOutputAsync(process, clipRing));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -239,6 +244,24 @@ public sealed class FfmpegEncoder : IFfmpegEncoder
|
||||
/// ArrayPool may return a larger buffer), returned to the pool after the write
|
||||
/// copies into the pipe, and stdin is closed (EOF) once the queue drains.
|
||||
/// </summary>
|
||||
private static async Task ReadClipOutputAsync(IEncoderProcess process, Services.ClipRing ring)
|
||||
{
|
||||
var splitter = new Fmp4FragmentSplitter(ring);
|
||||
var buffer = new byte[64 * 1024];
|
||||
try
|
||||
{
|
||||
var stdout = process.StandardOutputBytes;
|
||||
int read;
|
||||
while ((read = await stdout.ReadAsync(buffer).ConfigureAwait(false)) > 0)
|
||||
splitter.Write(buffer.AsSpan(0, read));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Clips stop; the broadcast does not.
|
||||
AppLog.Write(ex, "Clip buffer reader failed — replay clips disabled for this session");
|
||||
}
|
||||
}
|
||||
|
||||
private void StartDrainLoop(IEncoderProcess process)
|
||||
{
|
||||
_drainTask = Task.Run(async () =>
|
||||
|
||||
Reference in New Issue
Block a user