forked from Foxyz/sync_replay_project
Relay: viewers keep the file size (headers within 4 s, connection: close), retries back off, 412 re-resolves
Two server behaviours left ffmpeg (8.1/9.0) without the file size, after which every seek failed with "size -38" until a reload: - dart:io holds response headers until body bytes exist: a stalled source meant no headers within mpv's 5 s timeout. Relay byte responses are now written on the raw connection, headers with the first bytes or after 4 s, and a swap ends waiting reads with a plain close. - every response closed its socket without saying connection: close, so ffmpeg 8.x reused a dead connection for its next seek (pol's X clips). The server now says it whenever the request asks. A failed pull waits 250 ms doubling to 8 s instead of re-pulling at once (the ~130 ms storm that drew Bilibili's 412/403); 412 counts as an expired URL and re-resolves the page, at most once per 60 s. The app reloads a player once per media when ffmpeg reports the lost size anyway. P37: a read that waits past ffmpeg's retry budget still ends the file early. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5e3834e6ba
commit
e91539d113
17 files changed
+575
-63
No files matched your search
+34
-6
@@ -561,6 +561,12 @@ opening/initializing the selected video_out") is opened again once,
|
||||
500 ms later (`render: the video output was not up for the open`); the
|
||||
player error it raised clears when the media loads.
|
||||
|
||||
A player whose HTTP stream lost the file's size (ffmpeg's "Seek failed
|
||||
(to N, size -38)": a request of its that got no headers in time, from a
|
||||
server without the relay's header rule, or the connection reuse above)
|
||||
fails every seek until a fresh open, so the coordinator runs "Reload
|
||||
video" once per media for it (`mpvStreamLost`, `onStreamLost`).
|
||||
|
||||
Render mode: media_kit's Linux/Windows video output creates mpv's render
|
||||
context (OpenGL, else its software renderer) when the `VideoController` is
|
||||
created, and nothing in its API says which. The host therefore requests
|
||||
@@ -899,7 +905,7 @@ How bytes move:
|
||||
kept; segments are evicted by distance (behind costs 4x), sizes
|
||||
estimated from the bytes per second of the pieces in so far (before
|
||||
that 4 Mbit/s video, 160 kbit/s audio, which also splits the budget).
|
||||
A failed pull is retried after 1 s. Measured (mpv 0.41 through the
|
||||
A failed pull is retried with backoff, as for bytes. Measured (mpv 0.41 through the
|
||||
dedicated server, loopback, ~30 MB/s upstream): Mux's TS HLS (1080p,
|
||||
one muxed playlist) opened in 0.8 s, seeks 0.07 to 1.1 s; Akamai's DASH
|
||||
(1080p + aac, two playlists, EDL) 1.3 s, seeks 0.03 to 1.4 s; an X
|
||||
@@ -908,7 +914,11 @@ How bytes move:
|
||||
opened in 0.02 s from the buffer.
|
||||
- Filling a byte buffer: a viewer blocked on a slice first (oldest wait
|
||||
first, handing the source over at slice boundaries), then the pinned
|
||||
slices, then read-ahead from the slowest viewer. A viewer reads the
|
||||
slices, then read-ahead from the slowest viewer. A failed pull (the
|
||||
source's upstream dropping, a CDN refusing) is followed by the next
|
||||
one after 250 ms, doubling up to 8 s (`relayRetryDelay`), back to none
|
||||
once a slice or a pull completes: a failing CDN is not asked again
|
||||
every round trip (that drew Bilibili's 412/403). A viewer reads the
|
||||
slice being filled as its bytes arrive, not once it is whole (mpv's
|
||||
demuxer, blocked in a read, takes a seek only once that read returns).
|
||||
Nothing is fetched while nobody watches through the relay (a Jellyfin
|
||||
@@ -969,7 +979,23 @@ How bytes move:
|
||||
its source before opening its own player, and the source reads its
|
||||
tracks' sizes at once.
|
||||
- Source away (D6): viewers keep playing from the buffer; a request
|
||||
waits up to 60 s for missing bytes. The source member going `gone`
|
||||
waits up to 60 s for missing bytes.
|
||||
- A viewer's byte answer is written on the raw connection (relic
|
||||
`Hijack`, `_rangeResponse`): its headers go out with the first bytes,
|
||||
or after 4 s without them (`relayHeaderWaitMs`). ffmpeg gives up on a
|
||||
request whose headers do not come within mpv's 5 s `network-timeout`
|
||||
and then has lost the file's size for good (8.x and 9.0 do not restore
|
||||
it): every later seek fails with "Seek failed (to N, size -38)". With
|
||||
the headers in time, a stalled read only times out and is reconnected
|
||||
(`reconnect_*`, see "Media pipeline"), and plays on once the bytes come.
|
||||
The answer always says `connection: close` and closes after its body:
|
||||
ffmpeg asks for close, and ffmpeg 8.x, not told so, sends its next seek
|
||||
on the connection after reading a range to its end (an mp4 with a box
|
||||
after `mdat`, like X's clips: the 58-byte tail, then back to the
|
||||
start) and fails it the same way. Other answers say so too when asked
|
||||
(`closeWhenAsked`, every server route). A swap that ends the relay
|
||||
closes its viewers' answers plainly (`RelayEnded`, not logged as a
|
||||
broken read). The source member going `gone`
|
||||
does not end the relay: it lives through the source's grace
|
||||
(`media.relay.sourceGone`, `ServerHub.refreshRelaySource`). The source
|
||||
(`RelaySource`, kept by the media pipeline's `RelayFeeder` through the
|
||||
@@ -1150,9 +1176,11 @@ all three link modes. Nothing is bundled or installed by hand.
|
||||
track's playlist must be readable, else the swap is refused ("Live
|
||||
streams can't be passed on", DRM, or nothing a relay can read) with a
|
||||
hint to use "Everyone fetches it". The swap names the tracks; its relay
|
||||
source reads them with yt-dlp's headers (`PageRelayMedia`). On 403/410
|
||||
(expired format URLs, 6 h on YouTube; signed segment URLs) from any
|
||||
track it resolves the page again once for all tracks, finds the same
|
||||
source reads them with yt-dlp's headers (`PageRelayMedia`). On
|
||||
403/410/412 (expired format URLs, 6 h on YouTube; signed segment URLs;
|
||||
Bilibili's 412) from any track it resolves the page again once for all
|
||||
tracks (not again within 60 s: a further refusal fails the read and the
|
||||
server's backoff asks later), finds the same
|
||||
format ids with the same sizes and piece counts and roles (playlists
|
||||
re-read and re-mapped by index; else "the video changed on the page")
|
||||
and goes on from where it stopped. Format URLs are bound to the
|
||||
|
||||
+6
-5
@@ -227,11 +227,12 @@ Traps:
|
||||
so shell tests run the real player layout without it; otherwise test pure
|
||||
logic (`StateDiff`, coordinator without widgets).
|
||||
- A `SyncServer` started inside a test: relic reports an error writing a
|
||||
response (a relay response cut short when its relay is dropped at a
|
||||
swap or stop) to the zone the server started in, which fails the
|
||||
test; outside tests it is only logged. Start it in `runZonedGuarded`
|
||||
and fail on anything else (`queue_switch_e2e`, `relay_e2e`): a viewer's
|
||||
read is still in flight at the `stop` in tearDown.
|
||||
response (one cut short at `stop`: `/m`, `/j`; the relay's byte
|
||||
answers are written on the raw connection and end on their own) to
|
||||
the zone the server started in, which fails the test; outside tests it
|
||||
is only logged. Start it in `runZonedGuarded` and fail on anything
|
||||
else (`queue_switch_e2e`, `relay_e2e`): a viewer's read can still be
|
||||
in flight at the `stop` in tearDown.
|
||||
- The Link page probes a filled-in link (network, yt-dlp), which never
|
||||
answers on the widget-test clock: pass `AppShell(linkProbe:)` /
|
||||
`LibraryTab(probe:)` a fake, or `pumpAndSettle` never returns.
|
||||
|
||||
+6
-2
@@ -543,8 +543,12 @@ anything else means one file.
|
||||
one-file URL of a media with `tracks`) or piece (or `p/` of a byte
|
||||
track), 410 unknown session, 503 + `retry-after` the source never
|
||||
connected or the server could not read the link (60 s), or a piece did
|
||||
not arrive within 60 s. A byte request whose bytes do not arrive within
|
||||
60 s ends early. Bytes count toward `bytesServed`.
|
||||
not arrive within 60 s. A byte request's headers come with its first
|
||||
bytes, or after 4 s without them (under mpv's 5 s timeout); its body
|
||||
then waits for missing bytes, up to 60 s for each, else the connection
|
||||
is reset. Every byte answer says `connection: close` and closes after
|
||||
its body; a swap that ends the relay closes it early without a reset.
|
||||
Bytes count toward `bytesServed`.
|
||||
- `GET /r/<sid>/<token>/source` (WebSocket): the source's relay channel,
|
||||
dialled by the source (works behind NAT); `?q=<queueId>` for the warm
|
||||
relay of that item (or the media's relay started from it). Only that
|
||||
|
||||
@@ -58,6 +58,14 @@ Problems are numbered; delete a line when its fix ships.
|
||||
- P36 After a reconnect the app re-dials the warm relay of an Up next
|
||||
item that was already played ("relay channel refused: not the source of
|
||||
Up next item q8"): harmless, the feeder should end with its item.
|
||||
- P37 A relay viewer whose read waits on bytes the source does not send
|
||||
for longer than ffmpeg's reconnect budget (4 tries per read, about
|
||||
40 s with the 4 s header rule) gets an I/O error that mpv takes as the
|
||||
end of the file. Measured with mpv 0.41 / ffmpeg 9.0.2 and the source
|
||||
away about a minute across a seek: it played on; one read stalled past
|
||||
the budget ends the file. Options: a longer `reconnect_delay_max` for
|
||||
relay URLs only, or the end-of-file path reopening a relay viewer
|
||||
that stopped short while the server plays.
|
||||
|
||||
**Restructure** (in order; reviewed against the code). Truth lives in
|
||||
three places: the server (members, presence, gate, transport, queue incl.
|
||||
|
||||
Reference in new issue
Block a user