fix: avoid duplicate timeline seek

close #1421
This commit is contained in:
edde746
2026-06-27 07:05:06 +02:00
parent 3b611a1e18
commit 930534ee31
3 changed files with 90 additions and 3 deletions
@@ -78,6 +78,14 @@ extension _PlexVideoControlsPlaybackInputMethods on _PlexVideoControlsState {
}
}
Future<void> _seekToTimelinePosition(Duration position) {
final clamped = clampSeekPosition(widget.player, position);
final seekFuture = _seekToPosition(clamped, notifyCompletion: false);
_lastDispatchedTimelineSeek = clamped;
_lastDispatchedTimelineSeekFuture = seekFuture;
return seekFuture;
}
Future<void> _playOrPause() async {
if (!widget.player.state.playing && _rewindOnResume > 0) {
final target = widget.player.state.position - Duration(seconds: _rewindOnResume);
@@ -89,14 +97,39 @@ extension _PlexVideoControlsPlaybackInputMethods on _PlexVideoControlsState {
/// Throttled seek for timeline slider - executes immediately then throttles to 200ms
void _throttledSeek(Duration position) {
if (widget.isTranscoding) return;
if (widget.isTranscoding) {
_lastDispatchedTimelineSeek = null;
_lastDispatchedTimelineSeekFuture = null;
return;
}
_seekThrottle([position]);
}
/// Finalizes the seek when user stops scrubbing the timeline
void _finalizeSeek(Duration position) {
_seekThrottle.cancel();
unawaited(_seekToPosition(position));
final clamped = clampSeekPosition(widget.player, position);
if (shouldSkipDuplicateTimelineSeek(
isTranscoding: widget.isTranscoding,
lastDispatchedSeek: _lastDispatchedTimelineSeek,
finalSeek: clamped,
)) {
final seekFuture = _lastDispatchedTimelineSeekFuture;
if (seekFuture == null) {
widget.onSeekCompleted?.call(clamped);
return;
}
unawaited(
seekFuture.then<void>((_) {
if (!mounted) return;
widget.onSeekCompleted?.call(clamped);
}),
);
return;
}
unawaited(_seekToPosition(clamped));
}
void _holdTimelineScrub() {
+12 -1
View File
@@ -186,6 +186,15 @@ KeyEventResult handlePromptDismissBackKey(KeyEvent event, VoidCallback? onDismis
return handleBackKeyAction(event, onDismissPrompt);
}
@visibleForTesting
bool shouldSkipDuplicateTimelineSeek({
required bool isTranscoding,
required Duration? lastDispatchedSeek,
required Duration finalSeek,
}) {
return !isTranscoding && lastDispatchedSeek == finalSeek;
}
typedef PlaybackSourceChangeCallback =
Future<void> Function({
int? newMediaIndex,
@@ -443,6 +452,8 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
Timer? _edgeAdjustmentIndicatorClearTimer;
// Seek throttle
late final Throttle _seekThrottle;
Duration? _lastDispatchedTimelineSeek;
Future<void>? _lastDispatchedTimelineSeekFuture;
// Current marker state
MediaMarker? _currentMarker;
List<MediaMarker> _markers = [];
@@ -495,7 +506,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
_skipMarkerFocusNode = FocusNode(debugLabel: 'SkipMarkerButton');
_seekThrottle = throttle(
(Duration pos) {
unawaited(_seekToPosition(pos, notifyCompletion: false));
unawaited(_seekToTimelinePosition(pos));
},
const Duration(milliseconds: 200),
leading: true,
+43
View File
@@ -729,6 +729,49 @@ void main() {
});
});
group('shouldSkipDuplicateTimelineSeek', () {
test('skips a matching non-transcode final seek', () {
expect(
shouldSkipDuplicateTimelineSeek(
isTranscoding: false,
lastDispatchedSeek: const Duration(minutes: 7, seconds: 30),
finalSeek: const Duration(minutes: 7, seconds: 30),
),
isTrue,
);
});
test('does not skip matching transcode seek', () {
expect(
shouldSkipDuplicateTimelineSeek(
isTranscoding: true,
lastDispatchedSeek: const Duration(minutes: 7, seconds: 30),
finalSeek: const Duration(minutes: 7, seconds: 30),
),
isFalse,
);
});
test('does not skip when no matching seek was already dispatched', () {
expect(
shouldSkipDuplicateTimelineSeek(
isTranscoding: false,
lastDispatchedSeek: const Duration(minutes: 7),
finalSeek: const Duration(minutes: 7, seconds: 30),
),
isFalse,
);
expect(
shouldSkipDuplicateTimelineSeek(
isTranscoding: false,
lastDispatchedSeek: null,
finalSeek: const Duration(minutes: 7, seconds: 30),
),
isFalse,
);
});
});
group('SyncOffsetControl', () {
testWidgets('uses 100ms slider steps without rendering tick marks', (tester) async {
LocaleSettings.setLocaleSync(AppLocale.en);