Protocol 11: split-track relay (YouTube video+audio), chunked upstream reads
Relayed media can carry 2-4 byte tracks (video, audio): yt-dlp picks a split pair when no single-file format exists (h264 first), the source channel serves each track, the server keeps a buffer per track (budget split by size, 8 MiB floor) and viewers open an mpv edl:// of the track URLs. Expired URLs re-resolve once for all tracks. Upstream GETs are cut into <= http_chunk_size (10 MiB) ranges: YouTube throttles larger ones. Live streams and DRM are refused; HLS/DASH pieces are a follow-up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b2b77a6b3f
commit
b038706dff
19 files changed
+945
-286
No files matched your search
+61
-24
@@ -395,24 +395,49 @@ What gets relayed (`SessionCoordinator.swapPicked`):
|
||||
| Link, "Through me" | relayed from that member's computer; for links only they can open |
|
||||
| Jellyfin item (browser pick or pasted link) | own account first, then the server's login (`/j`), then relayed from the member who put it up (with their sign-in, or the pasted link's key, which stays on their machine) |
|
||||
|
||||
Relayed links are range-read as one file: a media file as is, a web page
|
||||
as yt-dlp's single-file format, resolved where the bytes are fetched (see
|
||||
"yt-dlp"); the swap carries only the page URL and the mode.
|
||||
Relayed links are range-read: a media file as is, a web page as yt-dlp's
|
||||
formats, resolved where the bytes are fetched (see "yt-dlp"); the swap
|
||||
carries only the page URL and the mode. A page's formats are one file, or
|
||||
a video-only and an audio-only file (YouTube has only those): a media of
|
||||
two **tracks**, each relayed as its own byte stream with its own buffer
|
||||
and URL, played together by viewers as an mpv EDL
|
||||
(`edl://!no_clip;%n%<video>;!new_stream;!no_clip;%n%<audio>`,
|
||||
`relayViewerUrl` / `mpvEdl`). media_kit opens an `edl://` URL unchanged
|
||||
(it writes it to a playlist and `loadlist`s it) and mpv keeps both
|
||||
streams in sync across seeks; one URL keeps the app's "one media URL"
|
||||
path (sync, swap, reopen) as is. The source member plays the page
|
||||
itself (yt-dlp hook) like any other link.
|
||||
|
||||
How bytes move:
|
||||
- The source dials `/r/.../source` (outbound, so NAT/CGNAT are fine) and
|
||||
answers the server's pulls from the file or the URL (`RelaySource`,
|
||||
`FileRelayBytes`, `HttpRelayBytes`, `LinkRelayBytes`; a Jellyfin source sends its own
|
||||
`Authorization` header from this machine). Viewers only ever talk HTTP
|
||||
to the server, so the source never sees mpv's request pattern.
|
||||
- The server keeps one `RelayBuffer` per relayed media: 1 MiB slices in
|
||||
answers the server's pulls from the file or the URL (`RelaySource` over
|
||||
a `RelayMedia`: its `RelayTrack`s, each a `RelayBytes`:
|
||||
`FileRelayBytes`, `HttpRelayBytes`, `LinkRelayMedia` / `PageRelayMedia`
|
||||
for links; a Jellyfin source sends its own `Authorization` header from
|
||||
this machine). Viewers only ever talk HTTP to the server, so the source
|
||||
never sees mpv's request pattern. The tracks of a media share the
|
||||
channel and its flow control (they compete for the same uplink).
|
||||
- Upstream GETs (`HttpRelayBytes.read`) are split into back-to-back
|
||||
ranges of at most yt-dlp's `http_chunk_size` for the format, else
|
||||
10 MiB (`relayUpstreamChunkBytes`); a buffer pull stays one read.
|
||||
YouTube serves a range up to 10 MiB at full speed but throttles a longer
|
||||
or open-ended one to about twice the bitrate (measured 0.8 MB/s against
|
||||
8.6 to 12.5 MB/s); chunked, a 1080p relay seeks in under 0.8 s (4 to
|
||||
6.6 s unchunked) and even beats mpv reading YouTube directly, since the
|
||||
buffer reads ahead at full speed.
|
||||
- The server keeps one `RelayBuffer` per track of a relayed media (one
|
||||
for a file): 1 MiB slices in
|
||||
memory, filled by one sequential pull at a time. Kept: the first and
|
||||
last 2 MiB (container indexes; mpv reads both on open and on seeks),
|
||||
64 MiB behind the slowest viewer and 256 MiB past the furthest, the
|
||||
same as each viewer's mpv buffer (`PlaybackController`), so a skip
|
||||
inside the viewers' buffer window is served from memory. Evicted by
|
||||
distance from the viewers, never LRU. With 2+ viewers the source sends
|
||||
each byte about once.
|
||||
each byte about once. Several tracks split the ahead and behind budgets
|
||||
by size (one duration, so by bitrate; `relayBudgets`), at least 8 MiB
|
||||
each, which keeps both windows at the same playback distance (1080p
|
||||
video + aac: about 246 + 10 MiB ahead); each pins its own head and
|
||||
tail. A viewer is its token in every track's buffer.
|
||||
- Filling: 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. Nothing is fetched while nobody
|
||||
@@ -431,7 +456,7 @@ How bytes move:
|
||||
stop. A crash frees it with the process.
|
||||
- Source away: viewers keep playing from the buffer; a request waits up
|
||||
to 60 s for missing bytes. The source reconnects with backoff (same
|
||||
token after a resume); a different file size drops the buffer.
|
||||
token after a resume); a different size drops that track's buffer.
|
||||
`state.media.relay.live` follows the channel, and viewers show "waiting
|
||||
for <name> to pass the video on" while it is down.
|
||||
|
||||
@@ -481,10 +506,17 @@ all three link modes. Nothing is bundled or installed by hand.
|
||||
`~/.cache/watch-together/yt-dlp`.
|
||||
- **Resolve** (`YtDlp.resolve`): `yt-dlp -J --no-playlist --no-warnings
|
||||
--ignore-config`, 25 s timeout: title, uploader, duration, thumbnail,
|
||||
and the best single-file format (`pickSingleFile`: video and audio not
|
||||
`none`, plain http(s), a video container, tallest up to 1080p) with its
|
||||
URL and `http_headers`; none when the page has only split or HLS
|
||||
streams (YouTube today).
|
||||
live or not, and what a relay reads (`pickTracks`), each format with
|
||||
its URL, `http_headers` and `http_chunk_size`. Only plain http(s)
|
||||
formats without DRM count:
|
||||
- the best single file (`pickSingleFile`: video and audio not `none`, a
|
||||
video container, tallest up to 1080p, else the shortest above), unless
|
||||
- a video-only + audio-only pair is taller (`pickSplit`): video tallest
|
||||
up to 1080p (else the shortest above), avc1 before vp9 before av01
|
||||
(hardware decoding), then bitrate; audio the original language, not a
|
||||
`-drc` variant unless alone, m4a/aac before opus, then bitrate.
|
||||
YouTube has only these (299 + 140 for a 1080p60 video).
|
||||
- Neither (only HLS/DASH playlists) or a live stream: nothing to relay.
|
||||
- **Probe** (`probeLink`, `link_probe.dart`): a `Range: bytes=0-0` GET; a
|
||||
video/audio type, or a generic one on a media file name, is a file
|
||||
(`looksLikeMediaFile`); anything else goes to yt-dlp, and when yt-dlp
|
||||
@@ -498,17 +530,22 @@ all three link modes. Nothing is bundled or installed by hand.
|
||||
(`script-opts=ytdl_hook-ytdl_path=...`, `ytdl-format=bv*[height<=1080]+ba/b`,
|
||||
once per player), waiting for the download unless the URL names a media
|
||||
file. A hook error shows as "can't open this link: ...".
|
||||
- **Through me**: the putter's probe must give a single-file format, else
|
||||
the swap is refused with a hint to use "Everyone fetches it". Its relay
|
||||
source reads that format with yt-dlp's headers (`PageRelayBytes`); on
|
||||
403/410 (expired format URL) it resolves the page again once and goes
|
||||
on from where it stopped.
|
||||
- **Through me**: the putter's probe must give tracks, else the swap is
|
||||
refused ("Live streams can't be passed on", or no video file: only
|
||||
streaming playlists) 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) from
|
||||
any track it resolves the page again once for all tracks, finds the
|
||||
same format ids with the same sizes (else "the video changed on the
|
||||
page") and goes on from where it stopped. Format URLs are bound to the
|
||||
resolving machine's address, so they never leave it.
|
||||
- **Server fetches it**: the server probes the link itself
|
||||
(`LinkRelayBytes`) with its own yt-dlp (off with `--no-yt-dlp` /
|
||||
`ytDlp: false`) and relays the single-file format the same way. The
|
||||
putter only refuses a page its own yt-dlp says has no single file (the
|
||||
server may reach what the putter cannot); when the server cannot read
|
||||
it, the putter gets an `err` with the reason. Viewers open a fetched
|
||||
(`LinkRelayMedia`) with its own yt-dlp (off with `--no-yt-dlp` /
|
||||
`ytDlp: false`) and relays its tracks the same way, setting
|
||||
`relay.tracks` with `live`. The putter only refuses a page its own
|
||||
yt-dlp says cannot be relayed (the server may reach what the putter
|
||||
cannot); when the server cannot read it, the putter gets an `err` with
|
||||
the reason. Viewers open a fetched
|
||||
link's `/r/` URL only once `relay.live` is set ("the server is getting
|
||||
the video…" until then): resolving takes seconds and media_kit's mpv
|
||||
gives up on a silent request after 5 s (`network-timeout`).
|
||||
|
||||
Reference in new issue
Block a user