Protocol 12: Up next (a shared queue) and where the video comes from

Up next: a server-side queue anyone edits by item id (add at end or
next, move, remove, play now, skip); each item says who added it and
how it plays. At the real end the first item swaps in after the end
card's countdown, put up by whoever can (its adder for through-me links
and files, someone with access for Jellyfin, else anyone); items that
can't play are skipped with the reason. The next item's link is
resolved ahead. Greyed "Continues with" episodes below. A chip with the
controls opens it as a popover (leaf_widgets, local path dependency for
now); "Add to queue" next to Put it up everywhere.

Delivery: the server measures the source's rate, the media's bitrate
and each member's read rate; the members card shows a source line
(who passes it on, through what, keeping up or not, source left) and
each member's route and buffer. A slow source is told once per media.
A failed server fetch is announced to everyone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-11 06:40:26 +03:00
1 parent 95cd668cfb
commit 6b10cae4e6
50 files changed
+4239 -157

No files matched your search

+34
View File
@@ -10,6 +10,7 @@ import 'package:web_socket/web_socket.dart';
import 'media_endpoint.dart';
import 'piece_buffer.dart';
import 'relay_buffer.dart';
import 'relay_stats.dart';
import 'session.dart';
final _log = createLogger('relay');
@@ -56,9 +57,39 @@ class ActiveRelay {
var _local = const <LocalRelayFeed>[];
RelicWebSocket? _channel;
var _channelFeeds = const <ChannelRelayFeed>[];
/// Shared by the tracks: time any of them waits on the source.
final clock = FeedClock();
final _rate = SourceRate();
ActiveRelay._(this.mediaId, this.name, this.sourceToken, this.onLiveChanged);
/// Takes the bytes the source sent so far, for [rate]; the server calls
/// it once a second.
void sampleRate() => _rate.sample(buffers.fold(0, (a, b) => a + b.bytesFromFeed), clock.busy);
/// Bytes per second the source delivers while the buffer asks for bytes
/// (`media.relay.rate`); 0 until measured.
int get rate => _rate.rate;
/// The media's average bytes per second for a [duration] in seconds
/// (`media.relay.need`): byte tracks by size, pieces tracks from the
/// pieces in so far; 0 while any track is unknown.
int need(double duration) {
if (buffers.isEmpty) return 0;
var total = 0.0;
for (final b in buffers) {
if (b is RelayBuffer) {
if (b.size <= 0 || duration <= 0) return 0;
total += b.size / duration;
} else if (b is PieceBuffer) {
final r = b.bytesPerSec;
if (r == null) return 0;
total += r;
}
}
return roundRate(total);
}
int _nextId() => ++_ids;
void _setTracks(List<RelayTrackSpec> tracks) {
@@ -69,6 +100,9 @@ class ActiveRelay {
for (final t in this.tracks)
t.pieces ? PieceBuffer(nextId: _nextId, rate: _piecesRate(t.kind)) : RelayBuffer(nextId: _nextId),
];
for (final b in buffers) {
b.clock = clock;
}
if (!_known.isCompleted) _known.complete();
}