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).
This commit is contained in:
edde746
2026-07-04 22:58:19 +02:00
parent 3cad91736b
commit f9abcd44fe
@@ -200,6 +200,8 @@ class DesktopVideoControlsState extends State<DesktopVideoControls> {
// 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<DesktopVideoControls> {
}
}
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() {