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.
103 lines
3.2 KiB
C#
103 lines
3.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
|
|
namespace ytLive.Services.Encoder;
|
|
|
|
/// <summary>
|
|
/// TASK 65: turns the fragmented-MP4 byte stream ffmpeg writes to stdout into a playable ring.
|
|
///
|
|
/// <para>MP4 boxes are length-prefixed, so this needs no muxer cooperation: <c>ftyp</c>+<c>moov</c> are
|
|
/// the initialisation segment, and each <c>moof</c>+<c>mdat</c> pair is one fragment. The ring only ever
|
|
/// sees whole fragments, which is what makes a snapshot playable and a trim safe.</para>
|
|
///
|
|
/// <para>Deliberately forgiving: an unparseable header clears the buffer and waits for the next one. A
|
|
/// clip that fails must never take the encoder down with it — the stream is running through the same
|
|
/// process.</para>
|
|
/// </summary>
|
|
public sealed class Fmp4FragmentSplitter
|
|
{
|
|
private readonly ClipRing _ring;
|
|
private readonly List<byte> _pending = new(capacity: 64 * 1024);
|
|
private readonly MemoryStream _init = new();
|
|
private readonly MemoryStream _fragment = new();
|
|
private bool _inFragment;
|
|
|
|
public Fmp4FragmentSplitter(ClipRing ring) => _ring = ring;
|
|
|
|
public void Write(ReadOnlySpan<byte> data)
|
|
{
|
|
foreach (var b in data) _pending.Add(b);
|
|
Drain();
|
|
}
|
|
|
|
private void Drain()
|
|
{
|
|
while (true)
|
|
{
|
|
if (_pending.Count < 8) return;
|
|
|
|
long size = ((long)_pending[0] << 24) | ((long)_pending[1] << 16) | ((long)_pending[2] << 8) | _pending[3];
|
|
var type = Encoding.ASCII.GetString(_pending.GetRange(4, 4).ToArray());
|
|
var header = 8;
|
|
|
|
if (size == 1)
|
|
{
|
|
if (_pending.Count < 16) return;
|
|
size = 0;
|
|
for (var i = 8; i < 16; i++) size = (size << 8) | _pending[i];
|
|
header = 16;
|
|
}
|
|
|
|
if (size < header || size > int.MaxValue)
|
|
{
|
|
// Not a box boundary any more; drop what we have and resync on the next write.
|
|
_pending.Clear();
|
|
return;
|
|
}
|
|
|
|
if (_pending.Count < size) return;
|
|
|
|
var box = _pending.GetRange(0, (int)size).ToArray();
|
|
_pending.RemoveRange(0, (int)size);
|
|
Route(type, box);
|
|
}
|
|
}
|
|
|
|
private void Route(string type, byte[] box)
|
|
{
|
|
switch (type)
|
|
{
|
|
case "ftyp":
|
|
_init.Write(box);
|
|
break;
|
|
case "moov":
|
|
_init.Write(box);
|
|
_ring.SetInit(_init.ToArray()); // init is complete once moov lands
|
|
break;
|
|
case "moof":
|
|
FlushFragment();
|
|
_fragment.Write(box);
|
|
_inFragment = true;
|
|
break;
|
|
case "mdat":
|
|
if (!_inFragment) return; // mdat without a moof is not replayable
|
|
_fragment.Write(box);
|
|
FlushFragment();
|
|
break;
|
|
default:
|
|
if (_inFragment) _fragment.Write(box);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void FlushFragment()
|
|
{
|
|
if (_inFragment && _fragment.Length > 0)
|
|
_ring.Append(_fragment.ToArray());
|
|
_fragment.SetLength(0);
|
|
_inFragment = false;
|
|
}
|
|
}
|