The Problem
When streaming secure HLS or DASH videos (e.g., .m3u8 playlists), simply passing a token on the initial playlist request is not enough. Each media segment (.ts) request also needs token authentication. If you're using ExoPlayer on Android or AVPlayer on iOS, and you're launching the video via a Flutter app, you might run into issues where:
- The playlist loads fine âś…
- But the segments fail to load ❌ due to missing headers
Android: ExoPlayer + Token in Segment Requests
Use a custom DataSource.Factory that injects your token into each segment request.
Kotlin
class ResolvingDataSource(
private val context: Context,
private val token: String
) : DataSource.Factory {
override fun createDataSource(): DataSource {
val defaultDataSourceFactory = DefaultHttpDataSource.Factory()
defaultDataSourceFactory.setDefaultRequestProperties(
mapOf("Authorization" to token)
)
return defaultDataSourceFactory.createDataSource()
}
}
When initializing ExoPlayer:
Kotlin
// Usage:
val mediaItem = MediaItem.fromUri(videoUrl)
val dataSourceFactory = ResolvingDataSource(context, "Bearer eyJhbGciOiJI...")
val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
exoPlayer.setMediaSource(mediaSource)
exoPlayer.prepare()
exoPlayer.playWhenReady = true
iOS: AVPlayer + Token Auth via Proxy
AVPlayer doesn’t allow direct header injection per segment. Workaround:
- Create a local proxy server using GCDWebServer.
- Download the original .m3u8 playlist, rewrite it to point to your local server.
- Intercept segment requests, append the token in headers, and fetch them from the real server.
Kotlin
func serveVideoLocally(from url: URL, token: String, completion: @escaping (URL) -> Void) {
let server = GCDWebServer()
server.addHandler(forMethod: "GET", pathRegex: "/.*", request: GCDWebServerRequest.self) { request in
let targetURL = resolveActualSegmentURL(request: request)
var req = URLRequest(url: targetURL)
req.setValue(token, forHTTPHeaderField: "Authorization")
let response = try? Data(contentsOf: targetURL)
return GCDWebServerDataResponse(data: response ?? Data(), contentType: "video/MP2T")
}
try? server.start(options: [GCDWebServerOption_Port: 8080])
let rewrittenPlaylistURL = rewritePlaylist(originalURL: url)
completion(rewrittenPlaylistURL)
}
func playHLS(from localURL: URL) {
let player = AVPlayer(url: localURL)
player.play()
}
Flutter Integration: Bridging the Gap
To connect your Flutter code with the custom native ExoPlayer (Android) and AVPlayer (iOS) logic, you’ll use MethodChannels. This allows Dart code to call native functions.
- Set Up Platform Channel in FlutterKotlin
import 'package:flutter/services.dart'; class NativeVideoPlayer { static const _channel = MethodChannel('com.rutu.native_video'); /// Plays video with token-authenticated HLS or DASH stream static Future<void> playVideo({ required String url, required String token, }) async { try { await _channel.invokeMethod('playVideo', { 'url': url, 'token': token, }); } on PlatformException catch (e) { print("Error playing video: ${e.message}"); } } }
- How to Use in Flutter UIKotlin
ElevatedButton( onPressed: () { NativeVideoPlayer.playVideo( url: 'https://yourdomain.com/secure/video.m3u8', token: 'Bearer eyJhbGciOiJIUzI1NiIsInR...', ); }, child: const Text('Play Video'), ),
- Android Native Code (MainActivity.kt or a dedicated platform handler)Kotlin
private val CHANNEL = "com.rutu.native_video" override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> if (call.method == "playVideo") { val url = call.argument<String>("url")!! val token = call.argument<String>("token")!! // Call your custom ExoPlayer setup with ResolvingDataSource playVideoWithToken(url, token) result.success(null) } else { result.notImplemented() } } } fun playVideoWithToken(url: String, token: String) { val context = this val mediaItem = MediaItem.fromUri(url) val dataSourceFactory = ResolvingDataSource(context, token) val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem) val player = ExoPlayer.Builder(context).build() player.setMediaSource(mediaSource) player.prepare() player.playWhenReady = true }
- iOS Native Code (AppDelegate.swift or a separate Swift file)Swift
let controller = window.rootViewController as! FlutterViewController let channel = FlutterMethodChannel(name: "com.rutu.native_video", binaryMessenger: controller.binaryMessenger) channel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in if call.method == "playVideo", let args = call.arguments as? [String: Any], let urlStr = args["url"] as? String, let token = args["token"] as? String, let videoURL = URL(string: urlStr) { serveVideoLocally(from: videoURL, token: token) { localURL in playHLS(from: localURL) result(nil) } } else { result(FlutterMethodNotImplemented) } }
- iOS Native Code (AppDelegate.swift or a separate Swift file)Swift
let controller = window.rootViewController as! FlutterViewController let channel = FlutterMethodChannel(name: "com.rutu.native_video", binaryMessenger: controller.binaryMessenger) channel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in if call.method == "playVideo", let args = call.arguments as? [String: Any], let urlStr = args["url"] as? String, let token = args["token"] as? String, let videoURL = URL(string: urlStr) { serveVideoLocally(from: videoURL, token: token) { localURL in playHLS(from: localURL) result(nil) } } else { result(FlutterMethodNotImplemented) } }