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:
1 parent
e52a7633a0
commit
d8585fb78e
17 files changed
+821
-151
No files matched your search
@@ -1,5 +1,5 @@
|
||||
abstract class RelayConstants {
|
||||
static const int protoVersion = 13;
|
||||
static const int protoVersion = 14;
|
||||
|
||||
// Relay (`/r/...`): a member's bytes through the server's buffer.
|
||||
static const int relaySliceBytes = 1 << 20;
|
||||
@@ -24,6 +24,12 @@ abstract class RelayConstants {
|
||||
/// A viewer request waits this long for its bytes (source away) before
|
||||
/// the server gives up on it.
|
||||
static const int relayWaitSecs = 60;
|
||||
/// The warm relay (Up next's first relayed item, before it goes up)
|
||||
/// keeps this much ahead of and behind its viewers' preloads, besides
|
||||
/// the pins: about what a preload player reads ahead (48 MiB), never the
|
||||
/// whole media.
|
||||
static const int relayWarmAheadBytes = 32 << 20;
|
||||
static const int relayWarmBackBytes = 8 << 20;
|
||||
|
||||
static const int seekDebounceMs = 300;
|
||||
static const double driftSeekThresholdSecs = 5.0;
|
||||
|
||||
@@ -182,7 +182,10 @@ class RelayTrackSpec {
|
||||
/// them, the server fills them in for [fetch] once it has read the link.
|
||||
/// [rate] and [need] are the server's measures, 0 while unknown: bytes per
|
||||
/// second the source (or the server's own fetch) delivers while the
|
||||
/// buffer asks for bytes, and the media's average bitrate.
|
||||
/// buffer asks for bytes, and the media's average bitrate. [queueId]:
|
||||
/// the Up next item this relay was started for (its warm relay) before
|
||||
/// it went up; viewers open its `/r/.../<queueId>/...` URLs, so their
|
||||
/// preloads of it carry on.
|
||||
class MediaRelay {
|
||||
final String source;
|
||||
final bool fetch;
|
||||
@@ -190,8 +193,15 @@ class MediaRelay {
|
||||
final List<RelayTrackSpec> tracks;
|
||||
final int rate;
|
||||
final int need;
|
||||
final String queueId;
|
||||
const MediaRelay(
|
||||
{this.source = '', this.fetch = false, this.live = false, this.tracks = const [], this.rate = 0, this.need = 0});
|
||||
{this.source = '',
|
||||
this.fetch = false,
|
||||
this.live = false,
|
||||
this.tracks = const [],
|
||||
this.rate = 0,
|
||||
this.need = 0,
|
||||
this.queueId = ''});
|
||||
|
||||
/// The source is measured slower than the media plays (both known):
|
||||
/// below 85 % of [need], so a rate close to it does not flicker.
|
||||
@@ -210,7 +220,8 @@ class MediaRelay {
|
||||
live: live ?? this.live,
|
||||
tracks: tracks ?? this.tracks,
|
||||
rate: rate ?? this.rate,
|
||||
need: need ?? this.need);
|
||||
need: need ?? this.need,
|
||||
queueId: queueId);
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
if (source.isNotEmpty) 'source': source,
|
||||
@@ -219,6 +230,7 @@ class MediaRelay {
|
||||
if (validTracks(tracks)) 'tracks': [for (final t in tracks) t.toJson()],
|
||||
if (rate > 0) 'rate': rate,
|
||||
if (need > 0) 'need': need,
|
||||
if (queueId.isNotEmpty) 'queueId': queueId,
|
||||
};
|
||||
|
||||
/// Unknown kinds, a single byte track or too many make it one file.
|
||||
@@ -232,7 +244,8 @@ class MediaRelay {
|
||||
live: json['live'] == true,
|
||||
tracks: ok ? List.unmodifiable(specs.cast<RelayTrackSpec>()) : const [],
|
||||
rate: _byteRate(json['rate']),
|
||||
need: _byteRate(json['need']));
|
||||
need: _byteRate(json['need']),
|
||||
queueId: json['queueId'] is String ? json['queueId'] as String : '');
|
||||
}
|
||||
|
||||
/// A bytes-per-second field: a finite non-negative number, else 0.
|
||||
|
||||
@@ -348,6 +348,9 @@ class RelaySource {
|
||||
|
||||
var _tracks = const <RelayTrack>[];
|
||||
|
||||
/// The media's tracks once [start] has opened it (empty before).
|
||||
List<RelayTrack> get tracks => _tracks;
|
||||
|
||||
/// Opens the media (a page is resolved) and starts serving; throws
|
||||
/// when it cannot be read.
|
||||
Future<void> start() async {
|
||||
|
||||
@@ -535,7 +535,9 @@ class SyncEngine {
|
||||
|
||||
/// [relay]: whether the server can relay the current Jellyfin media; the
|
||||
/// engine does not know the server's credential.
|
||||
StateBroadcast broadcast({bool relay = false, bool relayNext = false}) {
|
||||
/// [warm]: the server's warm relay for Up next's first item, shown as
|
||||
/// that item's `media.relay`.
|
||||
StateBroadcast broadcast({bool relay = false, bool relayNext = false, ({String id, MediaRelay relay})? warm}) {
|
||||
final bySeek = _seekEventId == eventId;
|
||||
return StateBroadcast(
|
||||
eventId: eventId,
|
||||
@@ -555,7 +557,10 @@ class SyncEngine {
|
||||
seekSeq: bySeek ? _seekSeq : null,
|
||||
relay: relay,
|
||||
relayNext: relayNext,
|
||||
queue: queue.items,
|
||||
queue: [
|
||||
for (final q in queue.items)
|
||||
warm != null && q.id == warm.id ? q.copyWith(media: q.media.copyWith(relay: warm.relay)) : q,
|
||||
],
|
||||
queuePlay: queue.play,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user