Sample Android app that plays DASH with Widevine and ClearKey using
AndroidX Media3 1.11.1
(androidx.media3:media3-exoplayer, media3-exoplayer-dash, media3-ui).
ExoPlayer 2 (com.google.android.exoplayer) is not used. DRM is configured with
the current Media3 APIs: MediaItem.DrmConfiguration, DefaultDrmSessionManager,
HttpMediaDrmCallback, and (for developer-supplied ClearKey JSON)
LocalMediaDrmCallback. Player failures in the DRM range are shown as
PlaybackException.ERROR_CODE_DRM_*.
| Piece | Out of the box | You must supply |
|---|---|---|
| Media3 player + DASH | Yes | — |
| Clear DASH (Tears of Steel) | Yes | — |
| Widevine Google test DASH + UAT license proxy | Yes (labeled as test) | Nothing, but the proxy is not a production license server; it can fail, rate-limit, or disappear |
Custom Widevine (drm.widevine.licenseUri) |
UI state MISSING LICENSE URL until set | A real license server URL that matches your content (optional manifest + license request headers) |
MediaItem.DrmConfiguration / DefaultDrmSessionManager |
Yes (code) | Matching license URI for your content |
| Widevine CDM | Device-dependent | Google Play system images usually include it; many AOSP emulators do not (MediaDrm.isCryptoSchemeSupported) |
| ClearKey HTTP license | API + empty Gradle placeholders | ClearKey-protected manifest and drm.clearkey.licenseUri |
| ClearKey local keys | API + LocalMediaDrmCallback |
Your own W3C ClearKey JSON via drm.clearkey.keysFile (gitignored). No keys are hardcoded |
| Production DRM / PlayReady on a phone | No | PlayReady SL2000 is documented for Android TV, not this sample |
This sample does not invent license servers, wrap keys, or fake MediaDrm.
The label above the status text is one of:
- CLEAR — unencrypted Google test DASH
- WIDEVINE · GOOGLE TEST — Media3 demo Tears of Steel +
proxy.uat.widevine.com - WIDEVINE · CUSTOM LICENSE — Gradle
drm.widevine.licenseUriis set - MISSING LICENSE URL — custom Widevine or ClearKey selected without a license URL or keys JSON; playback is not started
- CLEARKEY — developer-supplied license URL or local keys JSON
- CDM UNSUPPORTED —
MediaDrm.isCryptoSchemeSupportedis false (no fake fallback) - DRM ERROR —
ERROR_CODE_DRM_*from the player - PLAYBACK ERROR — other Media3
PlaybackExceptioncodes
These extras sit on top of the Media3 sample. They do not add a license server.
| Extra | What it does | Honest limit |
|---|---|---|
DrmSessionEventListener |
Session acquire / keys loaded / manager error / release, shown in the UI and via EventLogger on logcat |
Events never include license keys. Wired through AnalyticsListener because ExoPlayer does not expose addDrmSessionEventListener. |
| UUID + CDM properties | Prints Widevine / ClearKey UUID and MediaDrm strings securityLevel, hdcpLevel, maxHdcpLevel, version, systemId |
Properties are vendor-optional. Missing values show as n/a. PlayReady is not opened on phones. |
forceDefaultLicenseUri |
Checkbox: keep license HTTP on the app URI even if a DASH period lists LicenseUrl. MediaItem path uses DrmConfiguration.setForceDefaultLicenseUri; session-manager path uses HttpMediaDrmCallback(..., forceDefaultLicenseUrl). Media3 1.11.1 has no DefaultDrmSessionManager.Builder setter for this. |
The default Google Tears of Steel MPD is typically one period, so the flag is a no-op until you load a multi-period (or in-band license URL) asset. |
| License HTTP retries | LoadErrorHandlingPolicy with 4 DRM load attempts |
Installed only on the DefaultDrmSessionManager radio. The MediaItem.DrmConfiguration path still uses ExoPlayer’s built-in DRM load policy. |
From Media3 DRM documentation:
| Scheme | Typical API level | Formats |
|---|---|---|
Widevine cenc |
19+ | DASH, HLS (FMP4) |
Widevine cbcs |
25+ | DASH, HLS (FMP4) |
ClearKey cenc |
21+ | DASH |
| PlayReady SL2000 | Android TV | DASH, SmoothStreaming, HLS (FMP4) |
PlayReady is not implemented here; phones do not expose that stack the way Android TV does.
The Widevine and clear DASH URLs are the Tears of Steel assets published for the AndroidX Media3 demo:
- Encrypted:
https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd - Clear:
https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd - Widevine license (Google UAT test proxy):
https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test
Those URLs are used only by the two “Google test” radios. Custom Widevine does not silently fall back to the UAT proxy.
Requirements:
- Android Studio Ladybug / AGP 8.11-compatible IDE, or JDK 17+
compileSdk36 (required by Media3 1.11.1 AARs) and Kotlin 2.2.10- Android device or emulator, API 24+
- For Widevine: a device/emulator image that includes Widevine (Google Play system images are more likely than AOSP)
./gradlew :app:assembleDebug
./gradlew :app:testDebugUnitTestIn the app:
- Widevine DASH — Google test (MediaItem.DrmConfiguration) — recommended Media3 path.
- Widevine DASH — Google test (DefaultDrmSessionManager) — same stream with
HttpMediaDrmCallbackinstalled onDefaultMediaSourceFactory. - Custom Widevine DASH — requires
drm.widevine.licenseUri; otherwise MISSING LICENSE URL. - Clear DASH — same title without DRM.
- ClearKey DASH — requires your license URL or keys JSON.
- Force app license URI — optional. When checked,
MediaItem.DrmConfiguration.setForceDefaultLicenseUri(true)(and the matchingHttpMediaDrmCallbackflag on the session-manager radio) ignores a per-period LicenseUrl. Leave it off for the Google test MPD.
The status line, CDM property dump, and DRM session log are diagnostics only. They do not request extra licenses.
Add properties to gradle.properties (or pass -P on the Gradle command line) and rebuild:
drm.widevine.manifestUri=https://your-cdn.example/stream.mpd
drm.widevine.licenseUri=https://your-widevine-license.example/license
drm.widevine.licenseRequestHeaders=Authorization=Bearer your-token
drm.clearkey.manifestUri=https://your-cdn.example/clearkey.mpd
drm.clearkey.licenseUri=https://your-clearkey-license.example/licenseFor ClearKey without a license HTTP server, put a W3C ClearKey response in a
local file (already gitignored as local-clearkey.json) and point Gradle at it:
drm.clearkey.manifestUri=https://your-cdn.example/clearkey.mpd
drm.clearkey.keysFile=local-clearkey.jsonExpected JSON shape (fill in your k / kid values; do not commit production keys):
{"keys":[{"kty":"oct","k":"<base64url-key>","kid":"<base64url-kid>"}]}License request headers also belong on
MediaItem.DrmConfiguration.Builder.setLicenseRequestHeaders or
HttpMediaDrmCallback.setKeyRequestProperty. This sample does not add fake auth.
| Module | Version | Maven |
|---|---|---|
androidx.media3:media3-exoplayer |
1.11.1 | Google Maven |
androidx.media3:media3-exoplayer-dash |
1.11.1 | same group |
androidx.media3:media3-ui |
1.11.1 | same group |
Declared in gradle/libs.versions.toml.