fix(player): hold the Watch Together anchor while the host reloads (#1809)

An in-place source switch — audio, subtitle, version or quality — detaches
the host's player for the duration of the reload. `_broadcast` falls back to
a position of 0 when no player is attached, so any state published in that
window names 0:00 as the authoritative position and every guest hard-seeks
to the start of the item.

Heartbeats already suppress themselves while detached, which is why this
hides: the paths that leak the zero are the ones that answer on demand.
`onStateRequested`, `onPeerJoined` and `onReconnected` all broadcast
regardless of whether a player is attached, so a guest entering the player,
joining, or reconnecting mid-reload is the trigger.

Fall back to the last broadcast anchor instead. That field is only assigned
for untargeted broadcasts, so it holds the last position the room was
actually told, and the reload's own re-attach path already re-anchors from
it once the player comes back.
This commit is contained in:
Tolu Adegbehingbe
2026-08-06 05:54:17 +02:00
committed by GitHub
parent db4f7a643b
commit f5488cb7ff
2 changed files with 35 additions and 1 deletions
@@ -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', () {