Implementing HLS/DASH Video Streaming in Mobile Applications
HLS and DASH — protocols for adaptive streaming. Player doesn't load one file but manifest with segments, dynamically switching qualities based on network speed. This isn't simple MP4 download — buffering, ABR algorithms and network error handling are involved.
HLS vs DASH: What to Choose
| Parameter | HLS | DASH |
|---|---|---|
| Native iOS support | Yes (AVFoundation) | No (library needed) |
| Native Android support | No (ExoPlayer) | No (ExoPlayer) |
| Standard latency | 6–30 s | 2–10 s |
| Low-Latency HLS/DASH | LL-HLS: ~2 s | LL-DASH: ~1–3 s |
| DRM support | FairPlay (iOS), Widevine | Widevine, PlayReady |
In practice: if audience is iOS-dominant and no DRM — HLS without third-party libraries. If cross-platform and minimal latency needed — DASH via ExoPlayer/Shaka.
iOS: AVPlayer + HLS
AVPlayer plays HLS natively. Create AVPlayerItem(url: m3u8Url):
let asset = AVURLAsset(url: hlsURL, options: [
"AVURLAssetHTTPHeaderFieldsKey": ["Authorization": "Bearer \(token)"]
])
let item = AVPlayerItem(asset: asset)
player = AVPlayer(playerItem: item)
Monitor buffering — KVO on item.isPlaybackLikelyToKeepUp and item.loadedTimeRanges. When isPlaybackLikelyToKeepUp == false — show spinner, on true — hide.
Pre-load next video: create AVPlayerItem in advance, add to AVQueuePlayer — first segment of next video starts downloading in background.
Android: ExoPlayer + HLS/DASH
val player = ExoPlayer.Builder(context).build()
// HLS
val hlsItem = MediaItem.Builder()
.setUri("https://example.com/stream.m3u8")
.build()
// DASH
val dashItem = MediaItem.Builder()
.setUri("https://example.com/manifest.mpd")
.build()
player.setMediaItem(hlsItem)
player.prepare()
player.play()
ExoPlayer automatically chooses HlsMediaSource or DashMediaSource by URL extension. For explicit specification or custom headers:
val dataSourceFactory = DefaultHttpDataSource.Factory()
.setDefaultRequestProperties(mapOf("Authorization" to "Bearer $token"))
val hlsSource = HlsMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(uri))
Adaptive Bitrate
By default, ExoPlayer uses AdaptiveTrackSelection — switches quality at segment boundaries (usually 2–10 s intervals). Set minimum quality:
val trackSelector = DefaultTrackSelector(context)
trackSelector.parameters = trackSelector.buildUponParameters()
.setMaxVideoSizeSd() // no higher than 480p on weak devices
.build()
On iOS — AVPlayerItem.preferredPeakBitRate = 2_000_000 limits upper bitrate, useful for traffic saving.
Network Error Handling
Network is unstable — segment didn't load, manifest returned 403, CDN gave 5xx. ExoPlayer retries automatically with exponential backoff (DefaultLoadErrorHandlingPolicy). Custom policy:
class RetryPolicy : DefaultLoadErrorHandlingPolicy() {
override fun getRetryDelayMsFor(loadErrorInfo: LoadErrorInfo) =
if (loadErrorInfo.errorCount < 5) 1000L * loadErrorInfo.errorCount else RETRY_DELAY_UNSET
}
On iOS: AVPlayerItem.status == .failed → player.currentItem?.error — read NSError, show "Retry" button.
Timeline
HLS player on one platform with ABR and error handling — 2 days. Cross-platform (iOS + Android) with DASH, DRM and manual quality selection — 4–5 days.







