ExoPlayer (now part of AndroidX Media3) is the recommended media player library for Android. Its modular architecture lets you customize every stage of the media pipeline from source to renderer.
Architecture Overview
MediaSource → Extractor → LoadControl
↓
TrackSelector → Renderer[] → AudioOutput / Display
↓
MediaSession (optional, for background audio)
Key components:
MediaItem— describes what to play (URI, metadata, subtitles, DRM config)MediaSource— fetches and buffers media (HLS, DASH, Progressive)Extractor— parses container format (MP4, WebM, TS)Renderer— renders audio and video tracksTrackSelector— chooses which audio/video/subtitle tracks to playLoadControl— controls buffer sizes and loading strategy
Basic Setup with Media3
// implementation("androidx.media3:media3-exoplayer:1.3.1")
// implementation("androidx.media3:media3-ui:1.3.1")
// implementation("androidx.media3:media3-exoplayer-hls:1.3.1")
// implementation("androidx.media3:media3-exoplayer-dash:1.3.1")
class VideoFragment : Fragment() {
private lateinit var player: ExoPlayer
override fun onStart() {
super.onStart()
player = ExoPlayer.Builder(requireContext())
.build()
.also { exoPlayer ->
binding.playerView.player = exoPlayer // PlayerView from media3-ui
val mediaItem = MediaItem.Builder()
.setUri("https://cdn.example.com/video.m3u8") // HLS
.setMediaId("video_001")
.build()
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
exoPlayer.playWhenReady = true
}
}
override fun onStop() {
super.onStop()
player.release()
}
}
Adaptive Streaming (HLS / DASH)
ExoPlayer selects the right quality variant automatically based on bandwidth:
// DASH stream with custom track selection
val trackSelector = DefaultTrackSelector(context).apply {
setParameters(
buildUponParameters()
.setMaxVideoSizeSd() // limit to SD for low bandwidth
.setPreferredAudioLanguage("en")
.setPreferredTextLanguage("en")
)
}
val player = ExoPlayer.Builder(context)
.setTrackSelector(trackSelector)
.build()
// Override selection at runtime
val params = player.trackSelectionParameters.buildUpon()
.setMaxVideoBitrate(2_000_000) // limit to 2Mbps
.build()
player.trackSelectionParameters = params
DRM: Widevine
DRM (Digital Rights Management) prevents unauthorized copying of premium content. Widevine is the Google-backed DRM system used on Android.
val drmConfig = MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
.setLicenseUri("https://widevine.example.com/license")
.setLicenseRequestHeaders(mapOf("Authorization" to "Bearer $authToken"))
.setMultiSession(false) // set true for multi-period DASH
.build()
val mediaItem = MediaItem.Builder()
.setUri("https://cdn.example.com/encrypted.mpd") // DASH
.setDrmConfiguration(drmConfig)
.build()
player.setMediaItem(mediaItem)
Widevine security levels:
L1: Hardware-backed decryption — video stays in secure memory (required for 1080p+ content)L2: Not commonly usedL3: Software decryption — content may be capturable; limited to 540p by many services
// Check device's Widevine level
val securityLevel = MediaDrm.isCryptoSchemeSupported(C.WIDEVINE_UUID).let {
val mediaDrm = MediaDrm(C.WIDEVINE_UUID)
mediaDrm.getPropertyString("securityLevel") // "L1", "L2", or "L3"
}
Buffering and Load Control
val loadControl = DefaultLoadControl.Builder()
.setBufferDurationsMs(
/* minBufferMs = */ 15_000, // minimum buffer before playback starts/resumes
/* maxBufferMs = */ 50_000, // maximum buffer size
/* bufferForPlayback = */ 2_500, // required buffer to start playback
/* bufferForPlaybackAfterRebuffer = */ 5_000 // after a stall
)
.build()
val player = ExoPlayer.Builder(context)
.setLoadControl(loadControl)
.build()
Listening to Playback Events
player.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
when (playbackState) {
Player.STATE_BUFFERING -> showBufferingIndicator()
Player.STATE_READY -> hideBufferingIndicator()
Player.STATE_ENDED -> onVideoEnded()
Player.STATE_IDLE -> {}
}
}
override fun onPlayerError(error: PlaybackException) {
when (error.errorCode) {
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED -> showNetworkError()
PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED -> showDrmError()
else -> showGenericError(error)
}
}
})
Key Takeaways
| Concept | Rule |
|---|---|
| Media3 | Use androidx.media3 (not legacy com.google.android.exoplayer2) |
| Release player | Call player.release() in onStop() — leaking ExoPlayer is expensive |
| Widevine L1 | Required for HD DRM content; check with getPropertyString("securityLevel") |
| LoadControl | Tune buffer sizes for your content type; defaults work for most VOD |
| TrackSelector | Use setMaxVideoBitrate or setMaxVideoSizeSd to adapt for network conditions |
| DRM license headers | Send authorization alongside license requests; refresh before expiry |