summaryrefslogtreecommitdiff
path: root/player-android/lib/api
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/api
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/api')
-rw-r--r--player-android/lib/api/dio_client.dart39
1 files changed, 24 insertions, 15 deletions
diff --git a/player-android/lib/api/dio_client.dart b/player-android/lib/api/dio_client.dart
index c988cf0..7a46885 100644
--- a/player-android/lib/api/dio_client.dart
+++ b/player-android/lib/api/dio_client.dart
@@ -113,32 +113,48 @@ class _UnauthorizedInterceptor extends Interceptor {
/// Callers own the returned [Dio] and may add further interceptors on top.
/// Separating construction from usage (SRP) keeps this class testable.
class DioClient {
- DioClient({
+ factory DioClient({
required Uri baseUrl,
required TokenStorage storage,
required GlobalKey<NavigatorState> navigatorKey,
String loginRoute = '/login',
BaseOptions? baseOptions,
- }) : _dio = _buildDio(
- baseUrl: baseUrl,
- storage: storage,
- navigatorKey: navigatorKey,
- loginRoute: loginRoute,
- baseOptions: baseOptions,
- );
+ }) {
+ final jar = CookieJar();
+ final dio = _buildDio(
+ baseUrl: baseUrl,
+ storage: storage,
+ navigatorKey: navigatorKey,
+ loginRoute: loginRoute,
+ baseOptions: baseOptions,
+ cookieJar: jar,
+ );
+ return DioClient._(dio: dio, cookieJar: jar);
+ }
+
+ DioClient._({required Dio dio, required CookieJar cookieJar})
+ : _dio = dio,
+ _cookieJar = cookieJar;
final Dio _dio;
+ final CookieJar _cookieJar;
/// Exposes the underlying [Dio] so that [PlayerApiClient] can issue typed
/// requests without re-implementing the interceptor plumbing.
Dio get dio => _dio;
+ /// Exposes the cookie jar so consumers that bypass Dio (e.g. ExoPlayer via
+ /// just_audio, video_player, CachedNetworkImage) can still authenticate
+ /// against the session-cookie-protected media endpoints.
+ CookieJar get cookieJar => _cookieJar;
+
static Dio _buildDio({
required Uri baseUrl,
required TokenStorage storage,
required GlobalKey<NavigatorState> navigatorKey,
required String loginRoute,
BaseOptions? baseOptions,
+ required CookieJar cookieJar,
}) {
final options = (baseOptions ?? BaseOptions()).copyWith(
baseUrl: baseUrl.toString(),
@@ -147,13 +163,6 @@ class DioClient {
responseType: ResponseType.json,
);
- // The server's /api/v1/auth/login sets an HttpOnly Set-Cookie (session=...).
- // Browsers persist this automatically; on mobile we attach a CookieJar so
- // Dio replays the cookie on subsequent requests. Without this, every call
- // after login returns 401 because Dio discards cookies by default.
- // In-memory is sufficient: logout clears it, and we persist the bearer
- // token (for API-token auth) separately via flutter_secure_storage.
- final cookieJar = CookieJar();
return Dio(options)
..interceptors.addAll([
// Cookie manager runs first so the session cookie is replayed before