@@ -62,6 +62,7 @@ class VideoPlayerViewModel extends ChangeNotifier {
6262 Duration _currentDuration = Duration .zero;
6363 static const _saveInterval = Duration (seconds: 10 );
6464 DateTime ? _lastSaveTime;
65+ bool _isSeeking = false ;
6566
6667 // Stall detection
6768 Timer ? _stallDetectionTimer;
@@ -139,7 +140,7 @@ class VideoPlayerViewModel extends ChangeNotifier {
139140 }
140141
141142 void _onPositionChanged (Duration position) {
142- if (_isDisposed) return ;
143+ if (_isDisposed || _isSeeking ) return ;
143144
144145 final now = DateTime .now ();
145146 final timeSinceLastSave = _lastSaveTime != null ? now.difference (_lastSaveTime! ) : _saveInterval;
@@ -169,32 +170,46 @@ class VideoPlayerViewModel extends ChangeNotifier {
169170 final matchDurationMs = result.matchDurationMs;
170171
171172 if (targetPosition != Duration .zero && ! _isDisposed) {
172- // Wait for the player to be ready with duration
173- StreamSubscription <Duration >? sub;
174- sub = player.stream.duration.listen ((duration) {
175- if (duration != Duration .zero) {
176- bool shouldSeek = true ;
177-
178- // If it's a cross-provider match, verify duration similarity
179- if (isCrossProvider && matchDurationMs != null ) {
180- final difference = (duration.inMilliseconds - matchDurationMs).abs ();
181- // Allow 60 seconds difference (60,000 ms)
182- if (difference > 60000 ) {
183- shouldSeek = false ;
184- debugPrint ('Cross-provider sync skipped: Duration mismatch ($difference ms)' );
185- }
173+ _isSeeking = true ;
174+ try {
175+ // Wait for duration to be available for validation (up to 2 seconds)
176+ Duration duration = _currentDuration;
177+ if (duration == Duration .zero) {
178+ try {
179+ duration = await player.stream.duration
180+ .firstWhere ((d) => d != Duration .zero)
181+ .timeout (const Duration (seconds: 2 ));
182+ } catch (_) {
183+ debugPrint ('Timeout waiting for duration metadata' );
186184 }
185+ }
187186
188- if (shouldSeek) {
189- // Don't seek if we're near the end (> 98% watched)
190- if (targetPosition < duration * 0.98 ) {
191- player. seek (targetPosition);
192- debugPrint ('Resumed playback at ${ targetPosition . inSeconds }s${ isCrossProvider ? ' (synced)' : '' } ' );
193- }
187+ // Cross-provider validation
188+ if (duration != Duration .zero && isCrossProvider && matchDurationMs != null ) {
189+ final difference = ( duration.inMilliseconds - matchDurationMs). abs ();
190+ if (difference > 60000 ) {
191+ debugPrint ('Cross-provider sync skipped: Duration mismatch ($ difference ms) ' );
192+ return ;
194193 }
195- sub? .cancel ();
196194 }
197- });
195+
196+ // Don't resume if almost finished
197+ if (duration != Duration .zero && targetPosition > duration * 0.98 ) {
198+ debugPrint ('Resume skipped: Video > 98% completed' );
199+ return ;
200+ }
201+
202+ // Use the buffering-aware seek to prevent race conditions on initial load
203+ await _waitForBufferingAndSeek (targetPosition, true );
204+
205+ // Additional safety delay to let the player settle
206+ await Future .delayed (const Duration (milliseconds: 500 ));
207+ debugPrint ('Resumed playback at ${targetPosition .inSeconds }s${isCrossProvider ? ' (synced)' : '' }' );
208+ } finally {
209+ if (! _isDisposed) {
210+ _isSeeking = false ;
211+ }
212+ }
198213 }
199214 }
200215
0 commit comments