Reported as "the default video and the green screen video are playing at a hideously slow rate… I thought the playback timing issues were resolved?!" They were not: two independent faults, both in the shared clip pipe. 1. QUADRATIC COPY IN THE FRAME READER. `RawVideoFrameReader` re-concatenated the whole pending buffer on every `Feed`, and the pipe feeds it 64 KB at a time. A 1080p BGRA frame is 8 MB, so one frame cost ~127 copies of a growing buffer — roughly half a gigabyte of memcpy per frame. Measured on a 30 fps clip: 6 frames in 3 seconds (2 fps). The reader now fills one preallocated frame buffer and hands that array to the VideoFrame, so a frame is copied exactly once. Pipe reads are 256 KB. Same clip: 29 fps. 2. PACING SLEPT ON TOP OF THE WORK. A fixed 1/fps sleep after each frame adds the frame's own cost, so 20 ms of work on a 30 fps clip yields 1/(33+20) ≈ 19 fps — measured 20.9. Pacing is now deadline-based (`Helpers/FramePacer.cs`): wait only what is left of the frame's interval, resync when a frame overruns instead of bursting, never sleep longer than one interval. The alert clip shares this pipe, so it gained both fixes. Tests: `FramePacerTests` (subtract work, no drift, late → resync, zero duration, reset); `ChromaKeyRealClipTests.RealClip_PlaysAtItsNativeRate` (throughput floor of 15 fps on the real 30 fps clip — the old code sat at 2); the two pacing assertions now check "at most one interval" rather than an exact sleep, which is what deadline pacing means. Full suite: 574 passed, 0 failed, clean build.
62 lines
2.5 KiB
C#
62 lines
2.5 KiB
C#
namespace ytLive.Services;
|
|
|
|
/// <summary>
|
|
/// TASK 21: parses a stream of tightly-packed raw BGRA frames into
|
|
/// <see cref="VideoFrame"/>s. The consumer of an ffmpeg <c>-f rawvideo -pix_fmt
|
|
/// bgra</c> stdout pipe: every frame is exactly <c>width*height*4</c> bytes, so a
|
|
/// frame is emitted whenever that many bytes have accumulated. Pure and fully
|
|
/// deterministic, so it is tested with a synthetic byte stream — no ffmpeg, no
|
|
/// process, no audio device needed.
|
|
/// <para><b>Fill a preallocated frame, do not re-concatenate a pending buffer</b> (2026-11-05).
|
|
/// The previous version copied the whole accumulated partial frame on every <c>Feed</c>, so a
|
|
/// 1080p frame arriving in 64 KB reads cost ~64 copies of a growing 8 MB buffer — about 0.5 GB of
|
|
/// copying per frame, which capped playback at ~4 fps no matter what the pacing said (reported as
|
|
/// "playing at a hideously slow rate"; measured 6 frames in 3 s on a 30 fps clip). Now the cost is
|
|
/// one copy per frame plus one copy of the tail that spans reads.</para>
|
|
/// </summary>
|
|
public sealed class RawVideoFrameReader
|
|
{
|
|
private readonly int _frameBytes;
|
|
private readonly int _width;
|
|
private readonly int _height;
|
|
|
|
// Staging buffer for the frame currently being assembled. It BECOMES the emitted frame's
|
|
// pixel array, then a fresh one is allocated — so a frame is copied exactly once.
|
|
private byte[] _frame;
|
|
private int _count;
|
|
|
|
public RawVideoFrameReader(int width, int height)
|
|
{
|
|
_width = width;
|
|
_height = height;
|
|
_frameBytes = width * height * 4;
|
|
_frame = new byte[_frameBytes];
|
|
}
|
|
|
|
/// <summary>Feeds <paramref name="bytes"/> read from the pipe and returns any
|
|
/// whole frames reconstructed so far. Partial/pending bytes are kept for the
|
|
/// next call — frames never split or merge across reads.</summary>
|
|
public List<VideoFrame> Feed(byte[] bytes)
|
|
{
|
|
var frames = new List<VideoFrame>();
|
|
if (bytes.Length == 0) return frames;
|
|
|
|
var offset = 0;
|
|
while (offset < bytes.Length)
|
|
{
|
|
var take = Math.Min(_frameBytes - _count, bytes.Length - offset);
|
|
Buffer.BlockCopy(bytes, offset, _frame, _count, take);
|
|
_count += take;
|
|
offset += take;
|
|
|
|
if (_count < _frameBytes) continue;
|
|
|
|
frames.Add(new VideoFrame(_width, _height, _frame));
|
|
_frame = new byte[_frameBytes];
|
|
_count = 0;
|
|
}
|
|
|
|
return frames;
|
|
}
|
|
}
|