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:
@@ -777,7 +777,11 @@ class HostPlaybackCoordinator {
|
|||||||
anchorPositionMs = _pendingStartPositionMs ?? player?.position.inMilliseconds ?? 0;
|
anchorPositionMs = _pendingStartPositionMs ?? player?.position.inMilliseconds ?? 0;
|
||||||
anchorHostTimeMs = _pendingStartAtMs!;
|
anchorHostTimeMs = _pendingStartAtMs!;
|
||||||
} else {
|
} 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();
|
anchorHostTimeMs = _nowMs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -716,6 +716,36 @@ void main() {
|
|||||||
h.dispose();
|
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', () {
|
group('driver distraction', () {
|
||||||
|
|||||||
Reference in New Issue
Block a user