From f9abcd44fe0f2e7819e1bab02cf33b0ed8ec144a Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sat, 4 Jul 2026 22:58:19 +0200 Subject: [PATCH] fix(playback): hold timeline preview until playback settles at the target The preview cleared unconditionally 2s after a committed seek; on a device still buffering, the live position hasn't caught up yet, so the next key press re-based off the stale position and re-targeted the same spot - discarding the seek exactly on the slow hardware the coalescing targets. Re-arm the clear until the live position is near the target, with a 10s ceiling backstop (same settle pattern as LiveSeekAccumulator). --- .../desktop_video_controls.dart | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/widgets/video_controls/desktop_video_controls.dart b/lib/widgets/video_controls/desktop_video_controls.dart index 9a88e672..f621832a 100644 --- a/lib/widgets/video_controls/desktop_video_controls.dart +++ b/lib/widgets/video_controls/desktop_video_controls.dart @@ -200,6 +200,8 @@ class DesktopVideoControlsState extends State { // Release still flushes synchronously, so this adds no tap latency. static const _timelineSeekDebounce = Duration(milliseconds: 800); static const _timelinePreviewClearDelay = Duration(seconds: 2); + static const _timelinePreviewSettleTolerance = Duration(seconds: 3); + static const _timelinePreviewClearCeiling = Duration(seconds: 10); // Content strip state bool _contentStripVisible = false; @@ -472,14 +474,30 @@ class DesktopVideoControlsState extends State { } } - void _clearTimelinePreviewIfStill(Duration target) { + void _clearTimelinePreviewIfStill(Duration target, Duration elapsed) { if (!mounted || _timelinePreviewPosition != target) return; + // Hold the preview until playback has actually reached the committed + // target: clearing while a slow device is still buffering re-bases the + // next key-seek off the stale live position, silently discarding the + // seek that was just committed. The ceiling is a backstop for streams + // that never settle (mirrors LiveSeekAccumulator._scheduleClear). + final live = widget.player.state.position; + if ((live - target).abs() > _timelinePreviewSettleTolerance && elapsed < _timelinePreviewClearCeiling) { + _timelinePreviewClearTimer = Timer( + _timelinePreviewClearDelay, + () => _clearTimelinePreviewIfStill(target, elapsed + _timelinePreviewClearDelay), + ); + return; + } setState(() => _timelinePreviewPosition = null); } void _scheduleTimelinePreviewClear(Duration target) { _timelinePreviewClearTimer?.cancel(); - _timelinePreviewClearTimer = Timer(_timelinePreviewClearDelay, () => _clearTimelinePreviewIfStill(target)); + _timelinePreviewClearTimer = Timer( + _timelinePreviewClearDelay, + () => _clearTimelinePreviewIfStill(target, Duration.zero), + ); } void _flushTimelinePreviewSeek() {