Protocol 13: Up next preloads, so switching to it is instant
Every app gets the first Up next item ready itself (own Jellyfin plan, direct links, guests via /j when state.relayNext says the server's login can pass it on) and opens it in a hidden, muted, paused second player; the swap promotes that player instead of opening cold, so its buffer counts at once. Reports go out on open and when the buffer reaches the gate's need. SwitchTrace times each phase. Measured in the e2e harness: 1.5-3 s before, 35-103 ms with a preload. Relayed items still open cold for everyone but their source (ROADMAP). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4275abe470
commit
ddd7a21195
19 files changed
+1041
-142
No files matched your search
@@ -25,8 +25,9 @@ Response _plain(int status) => Response(status, body: Body.fromString('status $s
|
||||
|
||||
/// `GET /j/<sid>/<token>/<itemId>`: relays byte ranges of the current
|
||||
/// Jellyfin media from [credential]'s server to a viewer, for guests without
|
||||
/// their own access. Only the active ref's item on the credential's server,
|
||||
/// only for a valid viewer token: no open proxy. Range goes up as sent;
|
||||
/// their own access. Only the active ref's item, or Up next's first one
|
||||
/// ([next], so viewers can get it ready), on the credential's server, only
|
||||
/// for a valid viewer token: no open proxy. Range goes up as sent;
|
||||
/// status, Content-Range, Content-Length, Content-Type and Accept-Ranges
|
||||
/// come back as Jellyfin sent them (a 401/403 from Jellyfin is the server's
|
||||
/// credential failing, so the viewer gets 502). Bytes stream through without
|
||||
@@ -39,6 +40,7 @@ void mountJellyfinRelayRoute(
|
||||
SessionMap sessions,
|
||||
JellyfinCredential? Function() credential,
|
||||
HttpClient Function() client, {
|
||||
JellyfinRef? Function()? next,
|
||||
String pattern = '/j/:sid/:token/:itemId',
|
||||
void Function(String token, MediaLog log)? onLog,
|
||||
Duration timeout = const Duration(seconds: 20),
|
||||
@@ -56,12 +58,14 @@ void mountJellyfinRelayRoute(
|
||||
default:
|
||||
return _plain(401);
|
||||
}
|
||||
final ref = session.media?.jellyfin;
|
||||
final cred = credential();
|
||||
if (ref == null || cred == null || cred.serverId.isEmpty || cred.serverId != ref.serverId) {
|
||||
return _plain(404);
|
||||
}
|
||||
if (!_sameItemId(itemId, ref.itemId)) return _plain(403);
|
||||
if (cred == null || cred.serverId.isEmpty) return _plain(404);
|
||||
JellyfinRef? onLogin(JellyfinRef? r) => r != null && cred.serverId == r.serverId ? r : null;
|
||||
final current = onLogin(session.media?.jellyfin);
|
||||
final upNext = onLogin(next?.call());
|
||||
if (current == null && upNext == null) return _plain(404);
|
||||
final ref = [current, upNext].firstWhere((r) => r != null && _sameItemId(itemId, r.itemId), orElse: () => null);
|
||||
if (ref == null) return _plain(403);
|
||||
|
||||
final range = req.headers['range']?.join(',');
|
||||
final HttpClientRequest upReq;
|
||||
|
||||
@@ -30,8 +30,16 @@ class ServerHub {
|
||||
: engine = engine ?? SyncEngine();
|
||||
|
||||
/// The current media is a Jellyfin ref on the server [jellyfin] logs in to.
|
||||
bool get relayAvailable {
|
||||
final ref = session.media?.jellyfin;
|
||||
bool get relayAvailable => _onLogin(session.media?.jellyfin);
|
||||
|
||||
/// Up next's first item is a Jellyfin ref on that server: `/j` serves it
|
||||
/// too, so viewers without access get it ready.
|
||||
bool get relayNextAvailable => _onLogin(nextJellyfinRef);
|
||||
|
||||
/// Up next's first item's Jellyfin ref (null: none, or not a ref).
|
||||
JellyfinRef? get nextJellyfinRef => engine.queue.items.firstOrNull?.media.jellyfin;
|
||||
|
||||
bool _onLogin(JellyfinRef? ref) {
|
||||
final cred = jellyfin;
|
||||
return ref != null && cred != null && cred.serverId.isNotEmpty && cred.serverId == ref.serverId;
|
||||
}
|
||||
@@ -53,7 +61,7 @@ class ServerHub {
|
||||
: '$head: $why';
|
||||
}
|
||||
|
||||
StateBroadcast stateBroadcast() => engine.broadcast(relay: relayAvailable);
|
||||
StateBroadcast stateBroadcast() => engine.broadcast(relay: relayAvailable, relayNext: relayNextAvailable);
|
||||
|
||||
/// The relay of the current media (`/r/...`), if it is relayed.
|
||||
ActiveRelay? relay;
|
||||
|
||||
@@ -98,7 +98,7 @@ class SyncServer {
|
||||
mountAvatarRoute(_app, hub);
|
||||
mountArtRoute(_app, hub);
|
||||
mountJellyfinRelayRoute(
|
||||
_app, sessions, () => hub.jellyfin, () => _relayClient ??= HttpClient());
|
||||
_app, sessions, () => hub.jellyfin, () => _relayClient ??= HttpClient(), next: () => hub.nextJellyfinRef);
|
||||
// advertiseHost is only an explicit override; otherwise echo what the client dialled.
|
||||
String authority(String dialledHost) => config.advertiseHost.isNotEmpty
|
||||
? '${config.advertiseHost}:$boundPort'
|
||||
|
||||
Reference in new issue
Block a user