using System;
namespace ytLive.Helpers;
///
/// Paces a frame stream to wall-clock deadlines instead of sleeping a fixed interval after each
/// frame. The difference matters: a fixed sleep ADDS the frame's own decode/copy cost, so a 30 fps
/// clip with 20 ms of work per frame plays at 1/(33+20) ≈ 19 fps — which is what a 1080p video layer
/// did (measured 20.9 fps on a 30 fps clip, 2026-11-05). Here the target is start + n × frame,
/// so work is absorbed by the interval when it fits, and a stream that has fallen behind resyncs
/// instead of bursting to catch up.
///
public sealed class FramePacer
{
private long _nextTicks = long.MinValue;
/// How long to wait after emitting the frame at . Returns zero
/// when the frame is already due (or late).
public TimeSpan DelayFor(TimeSpan frameDuration, TimeSpan now)
{
if (frameDuration <= TimeSpan.Zero) return TimeSpan.Zero;
if (_nextTicks == long.MinValue)
{
_nextTicks = now.Ticks + frameDuration.Ticks;
return frameDuration;
}
var remaining = TimeSpan.FromTicks(_nextTicks - now.Ticks);
_nextTicks += frameDuration.Ticks;
if (remaining < TimeSpan.Zero)
{
// Late (the frame's work exceeded its interval): resync to now rather than emitting a
// burst of frames back-to-back, which the viewer would see as a speed-up.
_nextTicks = now.Ticks + frameDuration.Ticks;
return TimeSpan.Zero;
}
// Never wait longer than one interval, even if the caller's clock stood still (a fake delay
// in a test, or a suspended machine): a longer sleep would be a pause, not pacing.
return remaining > frameDuration ? frameDuration : remaining;
}
/// Restarts the schedule (a new playback pass begins at the top of the file).
public void Reset() => _nextTicks = long.MinValue;
}