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