Protocol 14: a warm relay preloads relayed Up next items for everyone

The server keeps one small relay (32 MiB ahead, 8 behind) for Up next's
first relayed item, keyed by its queue id at a URL that stays the same
after the swap: fetched by the server for server-fetch links, fed by
the adder's app for through-me links and files, by a member with an
account for member-passed Jellyfin items. Every app preloads from it;
the swap promotes it with its buffer. Dropped when the item stops
being first or its adder leaves. Measured switch: ~1.6 s cold, 20-40 ms
preloaded. ROADMAP gains the research item on server-side fetching of
member-resolved URLs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-11 17:53:29 +03:00
1 parent e52a7633a0
commit d8585fb78e
17 files changed
+821 -151

No files matched your search

@@ -680,13 +680,62 @@ class SessionCoordinator {
/// This viewer's `/r/` URL; with tracks, a pieces track's playlist, or
/// an mpv EDL of every track when there are several (a page's separate
/// video and audio).
String _relayMediaUrlFor(String name, int mediaId, [List<RelayTrackSpec> tracks = const []]) =>
relayViewerUrl('http://$_wsHost:$_wsPort/r/default/${sync.token}/$mediaId', name, tracks: tracks);
/// video and audio). A relay started in Up next ([queueId]) keeps its
/// queue id in the URL, so the preload of its warm relay matches.
String _relayMediaUrlFor(String name, int mediaId,
[List<RelayTrackSpec> tracks = const [], String queueId = '']) =>
relayViewerUrl('http://$_wsHost:$_wsPort/r/default/${sync.token}/${queueId.isEmpty ? mediaId : queueId}', name,
tracks: tracks);
/// This viewer's URL of the warm relay of Up next's first [item]
/// (`queue[0].media.relay`, see ARCHITECTURE "Getting ready"): the same
/// URL the media has once the item goes up, so the preload is promoted.
String warmRelayUrl(QueueItem item, MediaRelay relay) =>
_relayMediaUrlFor(item.media.name.isEmpty ? item.title : item.media.name, -1, relay.tracks, item.id);
// This member feeds the warm relay of Up next item `queueId` (its
// link, file or Jellyfin stream) before the item goes up; the swap
// that puts it up takes the source over.
({String queueId, RelaySource src})? _warmSource;
/// Feeds the warm relay of Up next item [queueId] from here with
/// [media]; one already feeding it is kept. The server pulls only while
/// someone preloads it, and closes the channel once the item is no
/// longer first (the source then stops).
void feedWarm(String queueId, RelayMedia media) {
if (_warmSource?.queueId == queueId) {
unawaited(media.close());
return;
}
stopWarmSource();
final src = RelaySource(
Uri.parse('ws://$_wsHost:$_wsPort/r/default/${sync.token}/source?q=${Uri.encodeQueryComponent(queueId)}'),
media);
_warmSource = (queueId: queueId, src: src);
unawaited(() async {
try {
await src.start();
} catch (e) {
debugPrint('warm relay source of $queueId failed: $e');
if (identical(_warmSource?.src, src)) _warmSource = null;
return;
}
await src.done;
if (identical(_warmSource?.src, src)) _warmSource = null;
}());
}
void stopWarmSource() {
final w = _warmSource;
_warmSource = null;
if (w != null) unawaited(w.src.close());
}
/// Serves [media] for media [mediaId] on this member's relay channel.
/// The server only pulls while someone watches through it.
void _startRelaySource(int mediaId, RelayMedia media, {bool announce = false}) {
/// The server only pulls while someone watches through it. When the
/// media came from Up next item [queueId] whose warm relay this member
/// feeds, that source carries on instead.
void _startRelaySource(int mediaId, RelayMedia media, {bool announce = false, String queueId = ''}) {
// Back after a reconnect on the same media: it is still serving (and
// redials by itself).
if (_relaySource != null && _relaySourceMediaId == mediaId) {
@@ -694,6 +743,24 @@ class SessionCoordinator {
return;
}
_stopRelaySource();
final warm = _warmSource;
if (queueId.isNotEmpty && warm != null && warm.queueId == queueId) {
_warmSource = null;
unawaited(media.close());
_relaySource = warm.src;
_relaySourceMediaId = mediaId;
relaying.value = true;
if (announce) {
_pushToast('Passing the video on through the server. Keep the app open while it plays.');
}
unawaited(warm.src.done.then((_) {
if (identical(_relaySource, warm.src)) {
_relaySource = null;
relaying.value = false;
}
}));
return;
}
final src = RelaySource(
Uri.parse('ws://$_wsHost:$_wsPort/r/default/${sync.token}/source'), media);
_relaySource = src;
@@ -811,15 +878,17 @@ class SessionCoordinator {
if (ref != null) {
await _playJellyfin(ref, r,
origin: origin is _JellyfinOrigin ? origin : null,
memberRelayUrl: relay != null && !mine ? _relayMediaUrlFor(name, r.mediaId) : null);
memberRelayUrl:
relay != null && !mine ? _relayMediaUrlFor(name, r.mediaId, const [], relay.queueId) : null);
} else if (origin is _FileOrigin) {
await _openMedia(origin.path);
_startRelaySource(r.mediaId, RelayMedia.single(FileRelayBytes(File(origin.path))), announce: true);
_startRelaySource(r.mediaId, RelayMedia.single(FileRelayBytes(File(origin.path))),
announce: true, queueId: relay?.queueId ?? '');
} else if (origin is _LinkOrigin) {
await _openMedia(origin.url);
_startRelaySource(
r.mediaId, LinkRelayMedia(Uri.parse(origin.url), ytDlp: origin.ytDlp, probe: origin.probe),
announce: true);
announce: true, queueId: relay?.queueId ?? '');
} else if (relay != null && !mine) {
if (relay.fetch && !relay.live) {
// The server is still getting it (yt-dlp takes seconds; mpv
@@ -828,7 +897,7 @@ class SessionCoordinator {
_awaitLiveFor = r.mediaId;
health.value = const HealthStatus(HealthSeverity.info, 'the server is getting the video…');
} else {
await _openMedia(_relayMediaUrlFor(name, r.mediaId, relay.tracks));
await _openMedia(_relayMediaUrlFor(name, r.mediaId, relay.tracks, relay.queueId));
}
} else {
var url = stateMediaUrl(r);
@@ -848,8 +917,9 @@ class SessionCoordinator {
if (_mediaId == r.mediaId) settledMediaId.value = r.mediaId;
} else if (_awaitLiveFor == r.mediaId && (stateMediaRelay(r)?.live ?? false)) {
_awaitLiveFor = -1;
final relay = stateMediaRelay(r)!;
await _openMedia(
_relayMediaUrlFor((r.media?['name'] as String?) ?? '', r.mediaId, stateMediaRelay(r)!.tracks));
_relayMediaUrlFor((r.media?['name'] as String?) ?? '', r.mediaId, relay.tracks, relay.queueId));
}
_jellyfinTransport(r);
_relayStatus(r);
@@ -1469,11 +1539,14 @@ class SessionCoordinator {
final relayProblem = probe.relayProblem;
if (relayProblem != null) return relayProblem;
final y = await ytDlp.ytDlp;
final check = LinkRelayMedia(uri, ytDlp: y, probe: probe);
// Up next's item this app already feeds (its warm relay): read.
final warm = _warmSource;
final fed = queueId != null && warm != null && warm.queueId == queueId && warm.src.tracks.isNotEmpty;
final check = fed ? null : LinkRelayMedia(uri, ytDlp: y, probe: probe);
var size = 0;
final List<RelayTrackSpec> specs;
try {
final tracks = await check.open().timeout(const Duration(seconds: 15));
final tracks = fed ? warm.src.tracks : await check!.open().timeout(const Duration(seconds: 15));
for (final t in tracks) {
// A playlist is read (DRM and live refused) but has no size.
final pieces = t.pieces;
@@ -1487,7 +1560,7 @@ class SessionCoordinator {
} catch (e) {
return 'cannot pass this link on: $e';
} finally {
await check.close();
await check?.close();
}
_pendingOrigin = _LinkOrigin(url, probe, y);
_swap(HelloMedia(
@@ -1717,7 +1790,8 @@ class SessionCoordinator {
// its own sign-in, else the pasted link (played directly here too).
if (origin != null) {
if (plan is OwnPlay) {
_startRelaySource(r.mediaId, RelayMedia.single(HttpRelayBytes(Uri.parse(plan.url), headers: plan.headers)));
_startRelaySource(r.mediaId, RelayMedia.single(HttpRelayBytes(Uri.parse(plan.url), headers: plan.headers)),
queueId: stateMediaRelay(r)?.queueId ?? '');
} else if (origin.link != null) {
plan = RelayPlay(origin.link!);
_startRelaySource(r.mediaId, RelayMedia.single(HttpRelayBytes(Uri.parse(origin.link!))));
@@ -2100,6 +2174,7 @@ class SessionCoordinator {
reconnecting.value = false;
await _stopJellyfin();
_stopRelaySource();
stopWarmSource();
_pendingOrigin = null;
_pendingArt = null;
_activeOrigin = null;