using System; using System.Collections.Generic; namespace ytLive.Services; /// /// TASK 65: the replay buffer's memory. ffmpeg writes fragmented MP4 to a pipe; the app keeps the last /// N seconds of those fragments here and hands them back whole. /// /// Why encoded bytes and not frames: a 1080p BGRA composite frame is 8.3 MB, so fifteen /// seconds at 60 fps is ~7.4 GB of raw frames. The 15 MB figure is the compressed stream, and only that /// fits in RAM. Why whole fragments: a fragment is the smallest thing that can be replayed, so the /// cap is a byte cap and the clip may be slightly shorter than the requested seconds — never corrupt. /// public sealed class ClipRing { private readonly long _capBytes; private readonly Queue _fragments = new(); private byte[]? _init; private long _bytes; public ClipRing(long capBytes) { if (capBytes <= 0) throw new ArgumentOutOfRangeException(nameof(capBytes)); _capBytes = capBytes; } /// The stream's initialisation segment (ftyp + moov). Every replayable file needs it, and /// it does not rotate — it is written once per encoder start. public void SetInit(byte[] init) { _init = init ?? throw new ArgumentNullException(nameof(init)); RecomputeBytes(); Trim(); } public void Append(byte[] fragment) { if (fragment is null || fragment.Length == 0) return; _fragments.Enqueue(fragment); _bytes += fragment.Length; Trim(); } public long Bytes => _bytes; public int FragmentCount => _fragments.Count; public bool IsEmpty => _init is null || _fragments.Count == 0; /// The bytes of a playable clip, init first. Empty array when nothing has been buffered — /// the caller must not write a zero-byte file (a failed save is better than a file that won't open). public byte[] Snapshot() { if (IsEmpty) return Array.Empty(); var total = _init!.Length; foreach (var fragment in _fragments) total += fragment.Length; var output = new byte[total]; var offset = 0; Buffer.BlockCopy(_init, 0, output, offset, _init.Length); offset += _init.Length; foreach (var fragment in _fragments) { Buffer.BlockCopy(fragment, 0, output, offset, fragment.Length); offset += fragment.Length; } return output; } public void Clear() { _fragments.Clear(); _init = null; _bytes = 0; } private void RecomputeBytes() { _bytes = _init?.Length ?? 0; foreach (var fragment in _fragments) _bytes += fragment.Length; } /// Drop oldest fragments until the ring fits. Newest data always wins: a clip that ends at /// the moment the creator pressed the key is the whole point. private void Trim() { while (_fragments.Count > 0 && _bytes > _capBytes) { _bytes -= _fragments.Dequeue().Length; } } }