refactor(watch-together): host-authoritative declarative sync protocol
Replaces the imperative play/pause/seek/positionSync message soup with a single host-authored PlaybackState (seq-ordered, anchor-extrapolated, phase machine: loading/waitingForPeers/paused/playing) that doubles as the heartbeat, plus guest status reports and host-applied control requests. Fixes the guest seek-back loop while the host loads (readiness was keyed on a pre-load !buffering snapshot and heartbeats broadcast frozen positions), adds real group buffering coordination (stall grace, scheduled simultaneous resumes, 15s safety timeout), rate-nudge drift correction with passthrough-aware seek fallback, session-scoped message handling (no lost messages during episode-switch detach gaps), and an expected-state ledger replacing the racy remote-action flag.
This commit is contained in:
@@ -23,13 +23,14 @@ void main() {
|
||||
expect(p.isHost, isFalse);
|
||||
expect(p.isConnected, isFalse);
|
||||
expect(p.isSyncing, isFalse);
|
||||
expect(p.isDeferredPlay, isFalse);
|
||||
expect(p.isWaitingForPeers, isFalse);
|
||||
expect(p.waitingOnNames, isEmpty);
|
||||
expect(p.isWaitingForHostReconnect, isFalse);
|
||||
expect(p.participants, isEmpty);
|
||||
expect(p.participantCount, 0);
|
||||
// Default control mode falls back to hostOnly when there's no session.
|
||||
expect(p.controlMode, ControlMode.hostOnly);
|
||||
expect(p.syncManager, isNull);
|
||||
expect(p.hasAttachedPlayer, isFalse);
|
||||
p.dispose();
|
||||
});
|
||||
|
||||
@@ -108,24 +109,24 @@ void main() {
|
||||
p.dispose();
|
||||
});
|
||||
|
||||
test('attachPlayer is a no-op without a sync manager (logs warning)', () {
|
||||
test('attachPlayer is a no-op without a sync controller (logs warning)', () {
|
||||
final p = WatchTogetherProvider();
|
||||
// The mpv Player object is platform-tied; skipping it would reach the
|
||||
// null-syncManager guard first and bail. Calling with a null check via
|
||||
// null-controller guard first and bail. Calling with a null check via
|
||||
// the same path used by the production code: just verify the early
|
||||
// return path on detachPlayer (which is also null-safe).
|
||||
expect(p.detachPlayer, returnsNormally);
|
||||
p.dispose();
|
||||
});
|
||||
|
||||
test('setBackgrounded forwards to sync manager but is null-safe', () {
|
||||
test('setBackgrounded forwards to the sync controller but is null-safe', () {
|
||||
final p = WatchTogetherProvider();
|
||||
expect(() => p.setBackgrounded(true), returnsNormally);
|
||||
expect(() => p.setBackgrounded(false), returnsNormally);
|
||||
p.dispose();
|
||||
});
|
||||
|
||||
test('onLocalSeek is null-safe without a sync manager', () {
|
||||
test('onLocalSeek is null-safe without a sync controller', () {
|
||||
final p = WatchTogetherProvider();
|
||||
expect(() => p.onLocalSeek(const Duration(seconds: 5)), returnsNormally);
|
||||
p.dispose();
|
||||
|
||||
Reference in New Issue
Block a user