androidengineers.Book a session

Media & Graphics

ExoPlayer Architecture & DRM

article25 minHard

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 tracks
  • TrackSelector — chooses which audio/video/subtitle tracks to play
  • LoadControl — 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 used
  • L3: 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

ConceptRule
Media3Use androidx.media3 (not legacy com.google.android.exoplayer2)
Release playerCall player.release() in onStop() — leaking ExoPlayer is expensive
Widevine L1Required for HD DRM content; check with getPropertyString("securityLevel")
LoadControlTune buffer sizes for your content type; defaults work for most VOD
TrackSelectorUse setMaxVideoBitrate or setMaxVideoSizeSd to adapt for network conditions
DRM license headersSend authorization alongside license requests; refresh before expiry

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
ExoPlayer Architecture & DRM | Android System Design | Android Engineers