diff --git a/lib/watch_together/models/sync_message.dart b/lib/watch_together/models/sync_message.dart index cf2a23c6..7d0e0aab 100644 --- a/lib/watch_together/models/sync_message.dart +++ b/lib/watch_together/models/sync_message.dart @@ -45,6 +45,9 @@ enum SyncMessageType { /// Player is ready (attached and loaded) playerReady, + + /// Request session config from host (guest recovery) + requestSessionConfig, } /// A message sent over the WebRTC data channel for synchronization @@ -181,12 +184,18 @@ class SyncMessage { } /// Create a SESSION_CONFIG message (sent by host to new guests) + /// + /// Optionally includes current media info so guests can catch up + /// if they missed a mediaSwitch broadcast. factory SyncMessage.sessionConfig({ required ControlMode controlMode, required Duration currentPosition, required bool isPlaying, required double playbackRate, String? peerId, + String? ratingKey, + String? serverId, + String? mediaTitle, }) { return SyncMessage( type: SyncMessageType.sessionConfig, @@ -196,6 +205,18 @@ class SyncMessage { bufferingState: !isPlaying, // Reuse field: false = playing, true = paused rate: playbackRate, peerId: peerId, + ratingKey: ratingKey, + serverId: serverId, + mediaTitle: mediaTitle, + ); + } + + /// Create a REQUEST_SESSION_CONFIG message (sent by guest to request current config from host) + factory SyncMessage.requestSessionConfig({String? peerId}) { + return SyncMessage( + type: SyncMessageType.requestSessionConfig, + timestamp: DateTime.now().millisecondsSinceEpoch, + peerId: peerId, ); } diff --git a/lib/watch_together/providers/watch_together_provider.dart b/lib/watch_together/providers/watch_together_provider.dart index 4b971a72..e978464b 100644 --- a/lib/watch_together/providers/watch_together_provider.dart +++ b/lib/watch_together/providers/watch_together_provider.dart @@ -356,6 +356,10 @@ class WatchTogetherProvider with ChangeNotifier { _handleSessionConfig(message); break; + case SyncMessageType.requestSessionConfig: + // Handled at sync manager level (host responds with config) + break; + default: break; } @@ -372,6 +376,18 @@ class WatchTogetherProvider with ChangeNotifier { _syncManager?.updateSession(_session!); // Update sync manager if it exists notifyListeners(); } + + // If config contains media info that differs from our current state, + // trigger a media switch so the guest navigates to the correct content. + // This handles the case where a guest missed a mediaSwitch broadcast + // (e.g., host switched episodes while guest was popping out of the player). + if (message.ratingKey != null && + message.serverId != null && + message.mediaTitle != null && + message.ratingKey != _session?.mediaRatingKey) { + appLogger.d('WatchTogether: Session config contains different media, triggering switch'); + _handleMediaSwitch(message); + } } /// Called when user seeks locally (to broadcast to peers) @@ -431,7 +447,17 @@ class WatchTogetherProvider with ChangeNotifier { appLogger.d('WatchTogether: Received media switch: ${message.mediaTitle}'); - // Update local session state + // Dispatch callback BEFORE updating session state. + // If the callback fails silently (e.g., player disposed mid-animation), + // the ratingKey stays unchanged so the next positionSync or re-sent + // message can retry instead of being treated as a duplicate. + if (onPlayerMediaSwitched != null) { + onPlayerMediaSwitched!(message.ratingKey!, message.serverId!, message.mediaTitle!); + } else { + onMediaSwitched?.call(message.ratingKey!, message.serverId!, message.mediaTitle!); + } + + // Update local session state after successful dispatch _session = _session?.copyWith( mediaRatingKey: message.ratingKey, mediaServerId: message.serverId, @@ -439,15 +465,6 @@ class WatchTogetherProvider with ChangeNotifier { ); notifyListeners(); - - // If player handler is set (VideoPlayerScreen is active), use that for proper navigation context - if (onPlayerMediaSwitched != null) { - onPlayerMediaSwitched!(message.ratingKey!, message.serverId!, message.mediaTitle!); - return; - } - - // Otherwise, trigger app-level navigation callback (MainScreen handles it) - onMediaSwitched?.call(message.ratingKey!, message.serverId!, message.mediaTitle!); } /// Notify guests that host is exiting the video player @@ -469,6 +486,11 @@ class WatchTogetherProvider with ChangeNotifier { appLogger.d('WatchTogether: Host exited player, callback set: ${onHostExitedPlayer != null}'); + // Clear the player callback BEFORE popping so that any mediaSwitch message + // arriving during the pop animation routes to MainScreen's handler instead + // of the dying VideoPlayerScreen. + onPlayerMediaSwitched = null; + // Trigger callback for the app to navigate guest out of player if (onHostExitedPlayer != null) { onHostExitedPlayer!.call(); diff --git a/lib/watch_together/services/watch_together_sync_manager.dart b/lib/watch_together/services/watch_together_sync_manager.dart index b203c943..de820719 100644 --- a/lib/watch_together/services/watch_together_sync_manager.dart +++ b/lib/watch_together/services/watch_together_sync_manager.dart @@ -136,6 +136,15 @@ class WatchTogetherSyncManager { // Note: playerReady will be announced when video loads (first buffering: false) + // If guest, request current session config from host in case we missed + // a mediaSwitch broadcast (e.g., host switched episodes while we were + // popping out of the previous player). + if (!_session.isHost) { + _peerService.broadcast( + SyncMessage.requestSessionConfig(peerId: _peerService.myPeerId), + ); + } + appLogger.d('WatchTogether: Player attached, isHost: ${_session.isHost}'); } @@ -504,6 +513,14 @@ class WatchTogetherSyncManager { await _checkAutoResume(); } break; + + case SyncMessageType.requestSessionConfig: + // Guest is requesting current session config (recovery after missed mediaSwitch) + if (_session.isHost && _hasAnnouncedReady && message.peerId != null) { + appLogger.d('WatchTogether: Guest ${message.peerId} requested session config, sending'); + _sendSessionConfig(toPeerId: message.peerId); + } + break; } } @@ -776,6 +793,9 @@ class WatchTogetherSyncManager { isPlaying: isPlaying, playbackRate: rate, peerId: _peerService.myPeerId, + ratingKey: _session.mediaRatingKey, + serverId: _session.mediaServerId, + mediaTitle: _session.mediaTitle, ); if (toPeerId != null) {