From 8d450ee93f095cfd2c5374b181d34ead9b49d502 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 22 May 2026 23:30:05 +0300 Subject: Pass session cookie to ExoPlayer/just_audio for authenticated streaming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- player-android/lib/api/dio_client.dart | 39 +++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 15 deletions(-) (limited to 'player-android/lib/api') 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 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 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 -- cgit v1.2.3