Skip to content

Commit 17c99d0

Browse files
committed
Add seeking flag to prevent conflicts during position changes [publish]
1 parent 8576163 commit 17c99d0

1 file changed

Lines changed: 38 additions & 23 deletions

File tree

packages/aimi_app/lib/viewmodels/video_player_viewmodel.dart

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)