diff --git a/lib/watch_together/services/host_playback_coordinator.dart b/lib/watch_together/services/host_playback_coordinator.dart index a2a50a47..50d40582 100644 --- a/lib/watch_together/services/host_playback_coordinator.dart +++ b/lib/watch_together/services/host_playback_coordinator.dart @@ -777,7 +777,11 @@ class HostPlaybackCoordinator { anchorPositionMs = _pendingStartPositionMs ?? player?.position.inMilliseconds ?? 0; anchorHostTimeMs = _pendingStartAtMs!; } else { - anchorPositionMs = anchorPositionOverrideMs ?? player?.position.inMilliseconds ?? 0; + // An in-place reload detaches the player, and the reply-on-demand paths + // still answer while it is gone. Without the last broadcast to fall back + // on, they publish an authoritative 0 and every guest hard-seeks to 0:00. + anchorPositionMs = + anchorPositionOverrideMs ?? player?.position.inMilliseconds ?? _lastBroadcast?.anchorPositionMs ?? 0; anchorHostTimeMs = _nowMs(); } diff --git a/test/watch_together/host_playback_coordinator_test.dart b/test/watch_together/host_playback_coordinator_test.dart index 38a2a1d3..25746c20 100644 --- a/test/watch_together/host_playback_coordinator_test.dart +++ b/test/watch_together/host_playback_coordinator_test.dart @@ -716,6 +716,36 @@ void main() { h.dispose(); }); }); + + test('the reply-on-demand paths hold the anchor while the player is detached', () { + fakeAsync((async) { + final h = _Harness(async); + h.attachForMedia(async, hasFirstFrame: true); + h.hostBecomesReady(async); + final anchored = h.last.anchorPositionMs; + expect(anchored, const Duration(minutes: 2).inMilliseconds); + + // The state every in-place source reload passes through. Heartbeats + // suppress themselves here; the on-demand replies do not, so they are + // the paths that would otherwise publish a position of 0. + h.coordinator.detachPlayer(); + async.flushMicrotasks(); + + h.coordinator.onStateRequested('guest'); + h.coordinator.onPeerJoined('guest2', compatible: true); + h.coordinator.onReconnected(); + async.flushMicrotasks(); + + for (final (state, toPeerId) in h.sent.skip(h.sent.length - 3)) { + expect( + state.anchorPositionMs, + anchored, + reason: 'reply to ${toPeerId ?? 'the room'} must hold the anchor, not collapse it to 0', + ); + } + h.dispose(); + }); + }); }); group('driver distraction', () {