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;
}
}
}