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:
foxyzandClaude Opus 5.5 committed 2026-10-11 16:15:16 +03:00
1 parent 4275abe470
commit ddd7a21195
19 files changed
+1041 -142

No files matched your search

@@ -0,0 +1,53 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
/// Timings of one media change on this client, from the `state` naming
/// the new media to the server playing it (see ARCHITECTURE.md "Getting
/// ready"). Printed when [enabled] (env `SYNC_RELAY_TRACE_SWITCH=1`);
/// marks are always kept for the last few media, tests read them.
class SwitchTrace {
/// Phases, in the order they usually happen.
static const state = 'state';
static const resolved = 'resolved';
static const open = 'open';
static const opened = 'opened';
static const preloaded = 'preloaded';
static const buffered = 'buffered';
static const playing = 'playing';
static bool enabled = (Platform.environment['SYNC_RELAY_TRACE_SWITCH'] ?? '').isNotEmpty;
final String who;
SwitchTrace([this.who = '']);
final Map<int, Map<String, int>> _marks = {};
final Map<int, Stopwatch> _clocks = {};
/// Milliseconds from [state] to each phase of media [mediaId].
Map<String, int> of(int mediaId) => Map.unmodifiable(_marks[mediaId] ?? const {});
/// Starts the clock for [mediaId] (its first `state`).
void start(int mediaId) {
if (_clocks.containsKey(mediaId)) return;
_clocks[mediaId] = Stopwatch()..start();
_marks[mediaId] = {state: 0};
while (_clocks.length > 4) {
final oldest = _clocks.keys.first;
_clocks.remove(oldest);
_marks.remove(oldest);
}
}
/// Records [phase] once for [mediaId]; [playing] ends the trace.
void mark(int mediaId, String phase) {
final clock = _clocks[mediaId];
final marks = _marks[mediaId];
if (clock == null || marks == null || marks.containsKey(phase)) return;
marks[phase] = clock.elapsedMilliseconds;
if (phase == playing && enabled) {
debugPrint('switch${who.isEmpty ? '' : ' $who'} m$mediaId: '
'${[for (final e in marks.entries) '${e.key} +${e.value} ms'].join(', ')}');
}
}
}