From 562d19c20724c71410b5375df0befcce5bfdce3d Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 15 May 2026 18:29:26 +0200 Subject: [PATCH] fix(watch-together): restore host resume close #1052 --- .../services/watch_together_sync_manager.dart | 20 ++++-- .../watch_together_sync_manager_test.dart | 61 +++++++++++++++++++ 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/lib/watch_together/services/watch_together_sync_manager.dart b/lib/watch_together/services/watch_together_sync_manager.dart index 10e8f0d6..591f806c 100644 --- a/lib/watch_together/services/watch_together_sync_manager.dart +++ b/lib/watch_together/services/watch_together_sync_manager.dart @@ -894,7 +894,7 @@ class WatchTogetherSyncManager { onSessionConfigReceived?.call(message.controlMode!); } - await _runGuardedRemoteAction( + final applied = await _runGuardedRemoteAction( actionName: 'session config', expectedAttachmentGeneration: expectedAttachmentGeneration, action: (player, attachmentGeneration) async { @@ -956,6 +956,10 @@ class WatchTogetherSyncManager { return true; }, ); + + if (applied) { + _reannounceReady(reason: 'session config'); + } } /// Set syncing state and notify listeners @@ -984,6 +988,14 @@ class WatchTogetherSyncManager { _backgrounded = value; } + void _reannounceReady({required String reason}) { + if (_hasAnnouncedReady && _peerService.myPeerId != null) { + _peerReady[_peerService.myPeerId!] = true; + _peerService.broadcast(SyncMessage.playerReady(peerId: _peerService.myPeerId!, ready: true)); + appLogger.d('WatchTogether: Re-announced player ready after $reason'); + } + } + /// Re-announce player readiness after reconnect. /// /// During reconnect the host resets our _peerReady entry to false via @@ -991,11 +1003,7 @@ class WatchTogetherSyncManager { /// reset because the player stays attached). Re-broadcast so the host /// doesn't stay stuck in the deferred-play gate. void reannounceReadyIfNeeded() { - if (_hasAnnouncedReady && _peerService.myPeerId != null) { - _peerReady[_peerService.myPeerId!] = true; - _peerService.broadcast(SyncMessage.playerReady(peerId: _peerService.myPeerId!, ready: true)); - appLogger.d('WatchTogether: Re-announced player ready after reconnect'); - } + _reannounceReady(reason: 'reconnect'); } /// Send join announcement to all peers diff --git a/test/watch_together/watch_together_sync_manager_test.dart b/test/watch_together/watch_together_sync_manager_test.dart index e596272d..2ee1f1a0 100644 --- a/test/watch_together/watch_together_sync_manager_test.dart +++ b/test/watch_together/watch_together_sync_manager_test.dart @@ -58,6 +58,67 @@ void main() { await peerService.close(); }); + test('ready guest re-announces readiness after receiving session config', () async { + final peerService = _FakeWatchTogetherPeerService(peerId: 'guest'); + final player = _FakePlayer(playing: false, position: const Duration(seconds: 10)); + final manager = _guestManager(peerService, controlMode: ControlMode.anyone); + + manager.initializeParticipants(['guest', 'host']); + manager.attachPlayer(player); + peerService.broadcasts.clear(); + + peerService.emit( + SyncMessage.sessionConfig( + controlMode: ControlMode.anyone, + currentPosition: const Duration(seconds: 20), + isPlaying: false, + playbackRate: 1.0, + peerId: 'host', + ), + ); + await _settle(); + + expect( + peerService.broadcasts.where( + (message) => + message.type == SyncMessageType.playerReady && + message.peerId == 'guest' && + message.bufferingState == true, + ), + isNotEmpty, + ); + expect(player.state.position, const Duration(seconds: 20)); + expect(player.state.playing, isFalse); + + manager.dispose(); + await player.dispose(); + await peerService.close(); + }); + + test('host local play is not deferred after guest readiness is restored', () async { + final peerService = _FakeWatchTogetherPeerService(peerId: 'host'); + final player = _FakePlayer(playing: false, position: const Duration(minutes: 3)); + final manager = _hostManager(peerService); + final deferredStates = []; + manager.onDeferredPlayChanged = deferredStates.add; + + manager.initializeParticipants(['host', 'guest']); + manager.attachPlayer(player); + peerService.emit(SyncMessage.playerReady(peerId: 'guest', ready: true)); + await _settle(); + peerService.broadcasts.clear(); + + await player.emitPlaying(true); + + expect(deferredStates, isNot(contains(true))); + expect(player.state.playing, isTrue); + expect(peerService.broadcasts.where((m) => m.type == SyncMessageType.play), isNotEmpty); + + manager.dispose(); + await player.dispose(); + await peerService.close(); + }); + test('removing a disconnected not-ready peer resumes deferred play', () async { final peerService = _FakeWatchTogetherPeerService(peerId: 'host'); final player = _FakePlayer(playing: false, position: const Duration(minutes: 5));