Skip to content

fix(playback): persist offline playback position across sessions - #623

Merged
RadicalMuffinMan merged 1 commit into
Moonfin-Client:patchfrom
mattsigal:feature/offline-progress-sync-fix
Jun 23, 2026
Merged

RadicalMuffinMan merged 1 commit into
Moonfin-Client:patchfrom
mattsigal:feature/offline-progress-sync-fix

Conversation

@mattsigal

Copy link
Copy Markdown
Collaborator

Pull Request

Summary

Fix offline playback progress not being persisted across sessions. After seeking to a position and exiting playback, re-entering the same item would always restart from 0:00 instead of resuming from the saved position.

Related Issues

Link related issues or tickets separated by commas.

Type of Change

  • Bug fix
  • New feature
  • Refactor
  • Performance improvement
  • UI/UX update
  • Documentation update
  • Build/CI change
  • Other (describe):

Changes Made

List the key changes included in this PR.

OfflinePlaybackTracker.startTracking() called the async stopTracking() without await. The deferred cleanup set _activeItemId = null after the new session had already assigned its item ID, causing every subsequent periodic save and the final exit save to silently return (because _activeItemId was null). Fixes applied across 5 files:

  • offline_playback_tracker.dart - Made startTracking() async and await stopTracking() to prevent the race condition. Also snapshots the manager position before cancelling the stream subscription in stopTracking(), and uses max(managerPos, streamPos) as the best position for saves.
  • offline_playback_launcher.dart - Reads the start position fresh from the DB via OfflineRepository.getItem() instead of from the potentially stale DownloadedItem passed by the UI. Awaits PlaybackManager.pendingStop first so any in-flight DB write from the previous exit has committed.
  • playback_manager.dart - Exposed pendingStop getter for the in-flight stop future. Added currentPlaybackPosition getter that returns the max of backend position, state position, and last known position. Moved _onOfflineStop callback to fire before backend.stop() so the tracker reads a live position. Set _lastKnownPosition in playOffline() for accurate resume.
  • video_player_screen.dart - Used context.canPop() with fallback to Navigator.of(context).pop() in _exitPlayback to handle both GoRouter and Navigator contexts.
  • connectivity_service.dart - Trigger offline-to-server progress sync when connectivity is restored and the server is reachable.

Platform

  • Android
  • iOS
  • tvOS
  • Web
  • macOS
  • Windows
  • Linux
  • All / Shared code

Testing

Describe how this change was tested.

  • Tested on emulator / simulator
  • Tested on physical device
  • Manual testing completed - Built on Android Mobile and tested against Windows Desktop
  • Not tested (explain why):

Test Steps

  1. Download a show/movie for offline viewing
  2. Exit the app and enable airplane mode
  3. Reopen the app and start offline playback
  4. Seek to ~15 minutes and press back to exit playback
  5. Re-enter the same item - it should resume from ~15 minutes
  6. Exit the app completely, reopen, verify the position is still saved
  7. Disable airplane mode - verify the progress syncs to the server

Screenshots (if applicable)

A tale in 6 images...

  1. partially played episode while online
  2. airplane mode on, showing playback status retained
  3. offline playback, fast forwarded to ~16 minutes in
  4. leaving playback, 16 minute timestamp retrained
  5. airplane mode off, 16 minute timestamp SYNCED
  6. Windows Desktop showing new timestamp!
Screenshot_20260622-184011 Screenshot_20260622-184213 Screenshot_20260622-184230 Screenshot_20260622-184242 Screenshot_20260622-184322 2026-06-22_18-48-38_moonfin

Checklist

  • Code builds successfully
  • Code follows project style and conventions
  • No unnecessary commented-out code
  • No new warnings introduced

Root cause: OfflinePlaybackTracker.startTracking() called the async
stopTracking() without await.  The deferred cleanup nullified
_activeItemId after the new session had already set it, causing every
periodic save and the final exit save to silently no-op.

Changes:
- Make startTracking() async and await stopTracking() so the previous
  session's DB write completes before the new session begins.
- Snapshot the manager position in stopTracking() before cancelling the
  stream subscription to capture the freshest position.
- Read the start position from the DB (via OfflineRepository.getItem)
  instead of the potentially stale DownloadedItem passed from the UI.
- Await PlaybackManager.pendingStop before the DB read so any in-flight
  stop from the unawaited exit flow has committed.
- Expose PlaybackManager.pendingStop getter for the above.
- Trigger offline-to-server progress sync when connectivity is restored.

Closes Moonfin-Client#376
@RadicalMuffinMan
RadicalMuffinMan merged commit 243b878 into Moonfin-Client:patch Jun 23, 2026
1 check passed
@mattsigal
mattsigal deleted the feature/offline-progress-sync-fix branch June 23, 2026 20:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants