Every authoritative message a guest acts on is gated on the relay-stamped
`senderId` matching `_session.hostPeerId` — room state, `hostExitedPlayer`
— except `pong`, which `_handleMessage` fed to `ClockSync` on nothing more
than "I am a guest and this pingId is one I am waiting for".
That matters because the guest's clock, not just the state it receives, is
part of the trust boundary. A guest estimates the host's clock offset from
the round trip: it sends `ping` at its own local time, the host answers
`pong` stamped with the host clock, and the guest takes the midpoint as the
one-way delay. Every anchor the host publishes — `anchorHostTimeMs` on a
state, a scheduled synchronized start — is translated into local time
through that offset, so a wrong offset silently shifts the target position
`GuestPlaybackReconciler` computes from otherwise authentic state. Past the
2000 ms hard-seek threshold the guest seeks, then keeps mistranslating the
corrections that follow, while the host and every other guest stay fine.
Any peer in the room could therefore reply to another guest's ping. This is
a weak primitive rather than playback takeover: the forged pong has to name
a `pingId` that is currently outstanding and land inside its RTT window,
and `ClockSync` discards samples over a second. But the relay already
stamps the sender on every inbound message, so the check costs one
conjunct.
Verified: full suite green (5788 tests, 5 skipped), 186 of them under
test/watch_together, plus dart format and analyzer parity.
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.
DD-3 gives video no exemption: a restricted vehicle must not play it at all. The
gate is read at the single point where media actually opens, so every path that
can start a picture - an explicit play, a gapless arm, a track or channel switch,
a frame-rate-match resume, a reload, and the queue navigation commands of the OS
media session - is covered by one check rather than by a guard at each call site.
A seek can also start playback with no play call, because mpv resumes when it
seeks off the end of a file, so a restricted seek is followed by a pause.
Watch Together needed the pause to be local. A vehicle stopping one peer is not a
room-wide intent: a guest's forced pause is swallowed by the attachment's ledger
rather than published, while a host's still pauses the room, because a host that
kept broadcasting a frozen anchor would stall or rewind every guest it was meant
to protect. The layer that owns a pause owns the resume for it, and one
acknowledgement is recorded per event, so a surplus cannot eat the user's next
real pause.
Consolidates duplicated logic behind shared implementations — paginated
grid tabs, focus chrome, cached remote stores, sheet selection columns,
the server artifact store and a test fixture layer — and removes code
that had become unreachable. Net reduction of about 5,500 lines with no
behaviour change.
Where a fix had landed separately in code that moved into a shared
helper, the fix was re-applied inside the helper rather than left behind
in the copy that went away.
Introduces shared seams for paginated views, D-pad reorder, media control
routing, async singletons and the device method channel, then points the
open-coded copies at them.
Also removes unused models and duplicated provider/server plumbing, folds
the twice-implemented artifact store in the server, and factors the
repeated Flutter toolchain prologue in CI into a composite action.
Drops dead code across services, models, utils and widgets, including the
connection auth service, which had no implementer, and the Live TV DVR
provisioning models, which had no caller.
Tests that only covered deleted behaviour are removed or trimmed. No
behaviour change.
Guest switch dispatch pre-marked its dedup key and fired-and-forgot, so
any failure (fetch error, reload busy with an auto-advance, navigation
race with the host exiting) silently stranded the guest on the old media.
A CurrentPlaybackDispatcher now marks a key handled only after the sink
reports success against the committed identity, with a serialized
in-flight slot, timeout, and generation reset; the reconciler re-offers
unattached media on every host heartbeat, making the heartbeat the retry
channel. Fetches that outlive their dispatch are re-validated against the
current snapshot so a stale switch can't override the live one.
hostExitedPlayer now rides the controller's ordered message queue with
host authentication instead of racing state handling in the provider.
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.
- Reset _firstPlayCompleted on detach so episode switches wait for peers
- Remove duplicate host join announcement from sync manager
- Use dedicated isPlaying field in sessionConfig with legacy fallback
- Replace Future.delayed with cancellable Timer to prevent sync indicator flicker
- Make Participant fields final and constructor const