Commit Graph
4 Commits
Author SHA1 Message Date
Tolu Adegbehingbe 85d1672909 fix(player): accept clock-sync pongs only from the host (#1850)
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.
2026-08-10 07:13:39 +02:00
Tolu Adegbehingbe 5c08b29756 fix(plex): keep compatible audio on capped transcodes (#1845)
`maxVideoBitrate` budgets the whole stream, so a capped transcode forced
even profile-compatible audio down to low-rate AAC (measured on Plex
1.43: EAC3 5.1 640k became 360k AAC). The client profile already carries
the video cap through `add-limitation(video.bitrate)`, so drop the
redundant `maxVideoBitrate` param and send `directStreamAudio=1`: the
video stays capped per preset while audio in the codecs the profile
declares (aac/ac3/eac3/mp3) is copied through untouched. Audio the HLS
target cannot carry still transcodes as before.
2026-08-10 07:05:23 +02:00
toluLikesToCode 8740a19f36 feat(player): start Plex transcodes at the resume position (#1817)
A Plex transcode session always starts producing at zero: the decision
request never sent offset=, so any non-zero open - resuming a
transcoded title, or switching from Direct Play to a transcoded
quality mid-playback - opened a session whose produced window begins
at the start of the file and seeked it. mpv immediately requests a
segment the transcoder has not produced, PMS answers 404 for it and
every subsequent segment, and playback buffers forever.

Send offset=<seconds> (6dp) with the decision and start request - the
view offset on initial open, the resolved resume position on every
in-place reload - so the session begins producing at the position the
player consumes first. The playlist timeline is unchanged: an offset
session's media playlist still covers the full title from segment
zero, so the player keeps opening with start: at the resume position
and in-stream seeks work as before.

Before a native player opens an offset playlist, waitForTranscodeReady
walks the master playlist, the media playlist, and the segment
containing the offset, because PMS can publish a manifest before that
segment is fetchable and mpv treats the 404 as an HLS error. The probe
is best-effort: it never fails an open, hands off immediately on HTTP
500 (on the response and exception paths alike) so the server-limit
dialog stays prompt, stops on cancellation, skips itself when the
playlist durations never reach the offset, and stays out of the
endpoint-failover cascade. In-place reloads resolve the replacement
source only after the old stop report has gone out, so Plex cannot use
that stop to terminate the replacement transcode.

close #1840
2026-08-09 06:28:59 +02:00
Tolu Adegbehingbe f5488cb7ff 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.
2026-08-06 05:54:17 +02:00