From f055759c7a470e97685e232290b2e8ba4dbf24fc Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:43:08 +0200 Subject: [PATCH] fix: suppress position sync when app backgrounded --- lib/screens/video_player_screen.dart | 5 +++++ .../providers/watch_together_provider.dart | 5 +++++ .../services/watch_together_sync_manager.dart | 16 +++++++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index e6472dbd..ef928ab1 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -472,6 +472,10 @@ class VideoPlayerScreenState extends State with WidgetsBindin return; } + // Suppress Watch Together heartbeats while backgrounded so App Nap + // doesn't cause stale position broadcasts that make guests loop. + _watchTogetherProvider?.setBackgrounded(true); + final currentPlayer = player; if (currentPlayer == null || !_isPlayerInitialized) { _recordLifecycleState('hidden', action: 'skipped_no_player'); @@ -511,6 +515,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin Future _handleAppResumed() async { _recordLifecycleState('resumed', action: 'begin'); + _watchTogetherProvider?.setBackgrounded(false); if (Platform.isAndroid && _androidAutoPipTransitionInFlight && !PipService().isPipActive.value) { _setAndroidAutoPipTransitionInFlight(false, reason: 'resume_without_pip'); diff --git a/lib/watch_together/providers/watch_together_provider.dart b/lib/watch_together/providers/watch_together_provider.dart index a2548335..b2d0c542 100644 --- a/lib/watch_together/providers/watch_together_provider.dart +++ b/lib/watch_together/providers/watch_together_provider.dart @@ -402,6 +402,11 @@ class WatchTogetherProvider with ChangeNotifier { appLogger.d('WatchTogether: Player detached from sync manager'); } + /// Suppress position sync while the app is backgrounded. + void setBackgrounded(bool value) { + _syncManager?.setBackgrounded(value); + } + /// Set up listeners for peer service events void _setupPeerServiceListeners() { _peerConnectedSubscription = _peerService!.onPeerConnected.listen((peerId) { diff --git a/lib/watch_together/services/watch_together_sync_manager.dart b/lib/watch_together/services/watch_together_sync_manager.dart index a67efe80..1d15eabd 100644 --- a/lib/watch_together/services/watch_together_sync_manager.dart +++ b/lib/watch_together/services/watch_together_sync_manager.dart @@ -79,6 +79,9 @@ class WatchTogetherSyncManager { // Whether we've announced our player as ready (first buffering: false) bool _hasAnnouncedReady = false; + // Whether the app is backgrounded (suppress heartbeats to avoid stale positions) + bool _backgrounded = false; + // Callbacks SessionConfigCallback? onSessionConfigReceived; SyncStateCallback? onSyncStateChanged; @@ -204,6 +207,7 @@ class WatchTogetherSyncManager { } _positionSyncTimer?.cancel(); _positionSyncTimer = null; + _backgrounded = false; appLogger.d('WatchTogether: Player detached'); } @@ -313,7 +317,7 @@ class WatchTogetherSyncManager { void _startPositionSync() { _positionSyncTimer?.cancel(); _positionSyncTimer = Timer.periodic(positionSyncInterval, (_) { - if (_player != null && _session.isHost) { + if (_player != null && _session.isHost && !_backgrounded) { _peerService.broadcast( SyncMessage.positionSync( _player!.state.position, @@ -956,6 +960,16 @@ class WatchTogetherSyncManager { } } + /// Suppress heartbeats while the app is backgrounded. + /// + /// macOS App Nap can throttle the event loop, causing stale position reads. + /// Guests would drift-correct to the stale position every heartbeat, making + /// playback loop. Pausing heartbeats avoids this; drift correction catches + /// up when the app returns to the foreground. + void setBackgrounded(bool value) { + _backgrounded = value; + } + /// Re-announce player readiness after reconnect. /// /// During reconnect the host resets our _peerReady entry to false via