summaryrefslogtreecommitdiff
path: root/player-android/lib/screens
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-22 23:30:05 +0300
committerPaul Buetow <paul@buetow.org>2026-05-22 23:30:05 +0300
commit8d450ee93f095cfd2c5374b181d34ead9b49d502 (patch)
tree40f303e291df57e73d0ae5e87bcfac0afe0c80c4 /player-android/lib/screens
parent9cd7ccc5d1150bf841a3961df9cb1e8a5d25abfe (diff)
Pass session cookie to ExoPlayer/just_audio for authenticated streaming
The audio/video players spawn a localhost proxy (just_audio) or call ExoPlayer directly (video_player) using their own HTTP stack, which does not share Dio's cookie jar. Without the session cookie those requests hit the stream endpoint anonymously and fail with 401. Expose the Dio CookieJar via a Riverpod provider (cookieJarProvider) and attach a Cookie header (alongside the existing Authorization: Bearer) to both AudioSource.uri and VideoPlayerController.networkUrl. Also enable android:usesCleartextTraffic="true" on the Application — just_audio's headers-injection proxy listens on 127.0.0.1 and Android 28+ blocks cleartext to it without the explicit opt-in. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'player-android/lib/screens')
-rw-r--r--player-android/lib/screens/audio_player_screen.dart26
-rw-r--r--player-android/lib/screens/video_player_screen.dart11
2 files changed, 29 insertions, 8 deletions
diff --git a/player-android/lib/screens/audio_player_screen.dart b/player-android/lib/screens/audio_player_screen.dart
index 9e28062..4f05abf 100644
--- a/player-android/lib/screens/audio_player_screen.dart
+++ b/player-android/lib/screens/audio_player_screen.dart
@@ -1,5 +1,6 @@
import 'dart:async';
+import 'package:cookie_jar/cookie_jar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:just_audio/just_audio.dart';
@@ -131,11 +132,12 @@ class _AudioPlayerScreenState extends ConsumerState<AudioPlayerScreen> {
final player = handler.player;
final client = ref.read(apiClientProvider);
final storage = ref.read(tokenStorageProvider);
+ final cookieJar = ref.read(cookieJarProvider);
final mediaIdInt = int.tryParse(widget.mediaId) ?? 0;
final url = widget.mediaUrl ?? client.streamUrl(mediaIdInt);
- // Step 1–2: build auth headers.
- final headers = await _buildAuthHeaders(storage);
+ // Step 1–2: build auth headers (Bearer + session cookie).
+ final headers = await _buildAuthHeaders(storage, cookieJar, Uri.parse(url));
if (!mounted) return;
// Step 3: load the authenticated source; show error UI on failure.
@@ -162,14 +164,26 @@ class _AudioPlayerScreenState extends ConsumerState<AudioPlayerScreen> {
_startProgressTicker(mediaIdInt, client, player, queue);
}
- /// Reads the bearer token and returns the `Authorization` header map.
+ /// Builds the headers map for an authenticated stream request.
///
- /// Returns an empty map when no token is stored so the source can still be
- /// loaded (e.g., public streams or during tests).
- Future<Map<String, String>> _buildAuthHeaders(TokenStorage storage) async {
+ /// just_audio runs a localhost proxy that forwards these headers to
+ /// ExoPlayer's underlying HTTP request, which is how we authenticate against
+ /// the session-cookie-protected `/api/v1/media/{id}/stream` endpoint without
+ /// sharing Dio's HTTP stack. Both Bearer (for API-token auth) and Cookie
+ /// (for session auth) are attached so either auth scheme works.
+ Future<Map<String, String>> _buildAuthHeaders(
+ TokenStorage storage,
+ CookieJar jar,
+ Uri url,
+ ) async {
final token = await storage.readToken();
+ final cookies = await jar.loadForRequest(url);
+ final cookieHeader = cookies
+ .map((c) => '${c.name}=${c.value}')
+ .join('; ');
return <String, String>{
if (token != null && token.isNotEmpty) 'Authorization': 'Bearer $token',
+ if (cookieHeader.isNotEmpty) 'Cookie': cookieHeader,
};
}
diff --git a/player-android/lib/screens/video_player_screen.dart b/player-android/lib/screens/video_player_screen.dart
index 763168d..2172a12 100644
--- a/player-android/lib/screens/video_player_screen.dart
+++ b/player-android/lib/screens/video_player_screen.dart
@@ -116,19 +116,26 @@ class _VideoPlayerScreenState extends ConsumerState<VideoPlayerScreen> {
final client = ref.read(apiClientProvider);
final storage = ref.read(tokenStorageProvider);
+ final cookieJar = ref.read(cookieJarProvider);
final mediaIdInt = int.tryParse(widget.mediaId) ?? 0;
// Step 1: resolve the stream URL — prefer the route-extra URL so the
// calling screen can forward a pre-computed URL; fall back to streamUrl.
final url = widget.mediaUrl ?? client.streamUrl(mediaIdInt);
- // Step 2: read the bearer token so the native player can authenticate
- // without routing bytes through Dart (performance and correctness).
+ // Step 2: read the auth artefacts so the native player can authenticate
+ // without routing bytes through Dart. Both Bearer (API-token auth) and
+ // Cookie (session auth) headers are attached because ExoPlayer has its
+ // own HTTP stack and does not share Dio's cookie jar.
final token = await storage.readToken();
+ final cookies = await cookieJar.loadForRequest(Uri.parse(url));
if (!mounted) return;
+ final cookieHeader =
+ cookies.map((c) => '${c.name}=${c.value}').join('; ');
final headers = <String, String>{
if (token != null && token.isNotEmpty) 'Authorization': 'Bearer $token',
+ if (cookieHeader.isNotEmpty) 'Cookie': cookieHeader,
};
// Step 3: create and initialise the VideoPlayerController.