Relay: a member's media through the server's buffer (proto 8)
Viewers who cannot reach the media get it through the server: a file on
any member's computer, a link passed on by a member ("Through me") or
fetched by the server ("Server fetches it", password-protected servers
only), and Jellyfin items for guests without an account, sourced by
whoever put them up with their own sign-in (or a pasted link's key),
which never leaves their machine. Fixes guests on a dedicated server
without a Jellyfin login being blocked.
The source dials /r/.../source (works behind NAT) and answers pulls with
an adaptive in-flight window (2 x rate x min RTT, 256 KiB..8 MiB). The
server keeps a memory-only buffer of 1 MiB slices sized like a viewer's
mpv buffer (pinned head/tail, 64 MiB back, 256 MiB ahead), fills it with
one sequential pull at a time (blocked viewers first, newest request
only), fetches nothing while nobody watches through it, and drops it on
the next swap or stop. Measured with mpv through shaped links
(tool/relay_e2e.dart): 400 Mbit/s seeks ~0.03 s; 40 Mbit/s no stalls,
3 viewers cost the source ~1x the bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9c5733719b
commit
e7f33b5dc3
27 files changed
+2438
-116
No files matched your search
@@ -93,7 +93,7 @@ void main() {
|
||||
final plan = await resolveJellyfinRef(accounts, episodeRef, relay: false, relayUrl: null);
|
||||
expect(plan, isA<BlockedPlay>());
|
||||
expect((plan as BlockedPlay).message,
|
||||
'This needs a Jellyfin account on that Jellyfin server (or the host can relay)');
|
||||
'No Jellyfin account here for that Jellyfin server, and nobody is passing it on');
|
||||
expect(plan.jfAccess, isNull);
|
||||
});
|
||||
|
||||
@@ -105,7 +105,7 @@ void main() {
|
||||
expect(plan, isA<RelayPlay>());
|
||||
expect(accounts.statusOf(fixtureServerId), JellyfinAccountStatus.expired);
|
||||
final blocked = await resolveJellyfinRef(accounts, episodeRef, relay: false, relayUrl: null);
|
||||
expect((blocked as BlockedPlay).message, contains('on Example Server'));
|
||||
expect((blocked as BlockedPlay).message, contains('for Example Server'));
|
||||
expect(blocked.message, contains('sign-in expired'));
|
||||
});
|
||||
|
||||
@@ -511,13 +511,13 @@ void main() {
|
||||
blocked: const BlockedPlay(serverId: fixtureServerId, serverName: 'Example Server'),
|
||||
onLinkAccount: () => link++,
|
||||
onRetry: () {})));
|
||||
expect(find.text('This needs a Jellyfin account on Example Server (or the host can relay)'),
|
||||
expect(find.text('No Jellyfin account here for Example Server, and nobody is passing it on'),
|
||||
findsOneWidget);
|
||||
await tester.tap(find.byKey(const ValueKey('link-jellyfin-account')));
|
||||
expect(link, 1);
|
||||
});
|
||||
|
||||
testWidgets('swap panel: the shared picker, File only for the host', (tester) async {
|
||||
testWidgets('swap panel: the shared picker, File for everyone, link relay choice', (tester) async {
|
||||
await setSize(tester, const Size(1400, 900));
|
||||
final (accounts, _) = await linkedAccounts();
|
||||
final c = MediaPickerController(source: MediaKind.link);
|
||||
@@ -535,14 +535,16 @@ void main() {
|
||||
onClose: () => closed++,
|
||||
)));
|
||||
await settle(tester);
|
||||
expect(find.widgetWithText(ButtonSegment<MediaKind>, 'File'), findsNothing);
|
||||
expect(find.text('File'), findsNothing);
|
||||
// A guest's file goes through the server's relay.
|
||||
expect(find.text('File'), findsOneWidget);
|
||||
expect(find.text('Jellyfin'), findsOneWidget);
|
||||
await tester.enterText(find.byType(TextField), 'https://cdn.example/movie.mkv');
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Through me'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byKey(const ValueKey('swap-go')));
|
||||
await settle(tester);
|
||||
expect(swapped, [const LinkMedia('https://cdn.example/movie.mkv')]);
|
||||
expect(swapped, [const LinkMedia('https://cdn.example/movie.mkv', relay: LinkRelay.me)]);
|
||||
expect(closed, 1);
|
||||
|
||||
// The Jellyfin source is the library browser; a pick goes up as is.
|
||||
@@ -554,6 +556,9 @@ void main() {
|
||||
controller: c, accounts: accounts, isHost: true, onSwap: (_) async => 'x', onClose: () {})));
|
||||
await settle(tester);
|
||||
expect(find.text('File'), findsOneWidget);
|
||||
await tester.tap(find.text('File'));
|
||||
await settle(tester);
|
||||
expect(find.text('Friends stream it from your computer.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('swap panel opens on the series of the episode playing, episode highlighted',
|
||||
|
||||
Reference in new issue
Block a user