Sessions leave redacted logs; viewers wait for a member relay's source before opening it

Logs: one redaction pass (query strings to names, member tokens,
passwords, cookies, auth headers) for every line; daily files with size
and age caps in the app (hosted server included) and the dedicated
server (--log-dir); swaps, relay channels, per-member /r /m /j outcomes,
server fetches, mpv load errors, yt-dlp runs. Settings > About: Copy
diagnostics, Open logs folder.

Bilibili through someone else: the viewer opened /r/ before the source
dialled in; the server holds that request and mpv gives up after 5 s
("could not be opened"). Viewers now open a member relay once it is
live, the putter starts its source before its own player, and track
sizes are read in parallel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-12 19:05:32 +03:00
1 parent 572104f49e
commit 4ab37c55cf
25 files changed
+1124 -108

No files matched your search

@@ -33,6 +33,8 @@ import 'now_playing.dart';
import 'now_playing_info.dart';
import 'switch_trace.dart';
final _log = createLogger('session');
/// Thrown when a connect is superseded by cancel/leave before it finished.
class ConnectCancelled implements Exception {
const ConnectCancelled();
@@ -387,7 +389,7 @@ class SessionCoordinator {
}
void _setPlayerError(String raw) {
debugPrint('player cannot open media $_mediaId: $raw');
_log.warning('player cannot open media $_mediaId: $raw');
_playerErrorFor = _mediaId;
_playerErrorDuration = playback.duration.peek();
playerError.value = playErrorText(raw);
@@ -563,6 +565,8 @@ class SessionCoordinator {
/// the player held before (a reconnect) keeps it.
Future<bool> _openMedia(String url, {Map<String, String>? headers}) async {
if (url == _openUrl) return true;
_log.info('media $_mediaId: open ${_urlKind(url, headers)}: ${logUrl(url)}'
'${headers == null || headers.isEmpty ? '' : ' (headers: ${headers.keys.join(', ')})'}');
final keepPlayer = _playerMediaId == _openedFor;
_openedFor = _playerMediaId;
_openUrl = url;
@@ -610,6 +614,23 @@ class SessionCoordinator {
_sendReport(r);
}
/// What [url] is, for the log.
String _urlKind(String url, Map<String, String>? headers) {
if (url.startsWith('edl://')) return 'relay (tracks)';
final u = Uri.tryParse(url);
if (u == null || !(u.isScheme('http') || u.isScheme('https'))) return 'local file';
if (u.host == _wsHost && u.port == _wsPort && u.pathSegments.isNotEmpty) {
return switch (u.pathSegments.first) {
'r' => 'relay',
'm' => "the host's file",
'j' => "Jellyfin through the server's login",
_ => 'server URL',
};
}
if (headers != null && headers.isNotEmpty) return 'Jellyfin, own sign-in';
return 'link, fetched here';
}
/// A web link (not this server's): mpv's yt-dlp hook gets the app's
/// managed yt-dlp, downloaded first if needed; a media file name does
/// not wait for that.
@@ -656,6 +677,12 @@ class SessionCoordinator {
/// The server-fetched media this viewer opens once it is live.
int _awaitLiveFor = -1;
/// A relay nobody feeds yet: the server's fetch before it is read, or a
/// member's before its channel is up while that member is still here
/// (once it left, the server's buffer is all there is: open that).
bool _waitForSource(StateBroadcast r, MediaRelay relay) =>
!relay.live && (relay.fetch || r.members.any((m) => m.id == relay.source));
/// Relayed through a member whose channel is down: say who everyone is
/// waiting for (the buffer still plays what it has).
void _relayStatus(StateBroadcast r) {
@@ -881,21 +908,27 @@ class SessionCoordinator {
memberRelayUrl:
relay != null && !mine ? _relayMediaUrlFor(name, r.mediaId, const [], relay.queueId) : null);
} else if (origin is _FileOrigin) {
await _openMedia(origin.path);
// The source first: viewers wait on it, not on this player.
_startRelaySource(r.mediaId, RelayMedia.single(FileRelayBytes(File(origin.path))),
announce: true, queueId: relay?.queueId ?? '');
await _openMedia(origin.path);
} 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, queueId: relay?.queueId ?? '');
await _openMedia(origin.url);
} else if (relay != null && !mine) {
if (relay.fetch && !relay.live) {
// The server is still getting it (yt-dlp takes seconds; mpv
// gives up on a silent request after 5): open once it is live.
if (_waitForSource(r, relay)) {
// The server is still getting it (yt-dlp takes seconds), or the
// member passing it on has not dialled in yet (its app opens the
// link first): mpv gives up on a silent request after 5 s, so
// open once it is live.
_openUrl = null;
_awaitLiveFor = r.mediaId;
health.value = const HealthStatus(HealthSeverity.info, 'the server is getting the video…');
final who = r.members.where((m) => m.id == relay.source).firstOrNull?.name;
_log.info('media ${r.mediaId}: waiting for ${relay.fetch ? 'the server' : who} before opening the relay');
health.value = HealthStatus(HealthSeverity.info,
relay.fetch ? 'the server is getting the video…' : 'waiting for $who to pass the video on…');
} else {
await _openMedia(_relayMediaUrlFor(name, r.mediaId, relay.tracks, relay.queueId));
}
@@ -915,7 +948,7 @@ class SessionCoordinator {
_seekGate.reset();
_clearPendingSeek();
if (_mediaId == r.mediaId) settledMediaId.value = r.mediaId;
} else if (_awaitLiveFor == r.mediaId && (stateMediaRelay(r)?.live ?? false)) {
} else if (_awaitLiveFor == r.mediaId && stateMediaRelay(r) != null && !_waitForSource(r, stateMediaRelay(r)!)) {
_awaitLiveFor = -1;
final relay = stateMediaRelay(r)!;
await _openMedia(