Day 3 · 22 min read

Android License Workflow

End-to-end flow on Android, online and offline.

Online playback — end to end

For online playback the workflow is the MediaDrm session lifecycle wrapped in the wider media stack:

Online Android playback
Step 1 of 8

AppBuild manifest URL + auth token

Decide which content, prove the user is entitled to it.

ExoPlayer integration sketch

val mediaDrmCallback = HttpMediaDrmCallback(LICENSE_URL, httpDataSourceFactory)
    .apply {
        setKeyRequestProperty("Authorization", "Bearer $appToken")
    }

val drmSessionManager = DefaultDrmSessionManager.Builder()
    .setUuidAndExoMediaDrmProvider(C.WIDEVINE_UUID, FrameworkMediaDrm.DEFAULT_PROVIDER)
    .setMultiSession(true)            // helps with key rotation in live
    .build(mediaDrmCallback)

val mediaSource = DashMediaSource.Factory(httpDataSourceFactory)
    .setDrmSessionManagerProvider { drmSessionManager }
    .createMediaSource(MediaItem.fromUri(MANIFEST_URL))

player.setMediaSource(mediaSource)
player.prepare()
player.play()

The integrator's surface area is small but the hooks matter:

  • setKeyRequestProperty — attach the auth token here.
  • setMultiSession(true) — required when key rotation creates multiple KIDs in a single playback.
  • setForceSessionsForAudioAndVideoTracks — when audio and video have different KIDs.

Offline playback

The offline flow has two parts: download time and replay time.

At download time

val drm = MediaDrm(C.WIDEVINE_UUID)
val sessionId = drm.openSession()
val req = drm.getKeyRequest(
    sessionId, initData, "video/mp4",
    MediaDrm.KEY_TYPE_OFFLINE, null
)
val resp = httpPost(licenseUrl, req.data)
val keysetId = drm.provideKeyResponse(sessionId, resp)   // returns keysetId
drm.closeSession(sessionId)
saveKeysetIdForContent(contentId, keysetId)

At replay time (offline)

val drm = MediaDrm(C.WIDEVINE_UUID)
val sessionId = drm.openSession()
drm.restoreKeys(sessionId, savedKeysetId)
val crypto = MediaCrypto(C.WIDEVINE_UUID, sessionId)
codec.configure(format, surface, crypto, 0)
// decode normally; no network

Releasing the offline keys

When the user finishes a rental window or deletes the download:

val req = drm.getKeyRequest(
    null /* scope */, savedKeysetId,
    null, MediaDrm.KEY_TYPE_RELEASE, null
)
val resp = httpPost(licenseUrl, req.data)
drm.provideKeyResponse(null, resp)

The release flow is required by some studio policies — it tells the licence server "this device has surrendered its persistent licence." Without it, telemetry on offline usage is incomplete.

Renewal / key rotation in Android

When the CDM emits EVENT_KEY_REQUIRED, ExoPlayer re-runs the key request flow with the existing session. The application typically does nothing special — but the licence server must accept renewal requests on the same session and return fresh keys.

For live with key rotation, setMultiSession(true) lets ExoPlayer hold one session per active KID.

Common pitfall

Forgetting setMultiSession(true) on a live stream with rotation will produce intermittent decoder errors at every KID boundary.

No questions yet for android-license. Add some in content/questions/android-license.json.