Two layers that are easy to conflate
ExoPlayer handles everything around the media: it parses the manifest, schedules segment loads, selects tracks and drives decoders. It does not implement DRM. Android provides that through android.media.MediaDrm, and ExoPlayer talks to it through interfaces it defines itself. Separating the two layers is the first step in reading a protected playback flow, because the traffic you can capture belongs to the player while the material you cannot read belongs to the platform component.
In Media3 the player lives under androidx.media3.exoplayer, with the DRM interfaces in the drm subpackage. Apps on the older ExoPlayer2 release use the same class names under com.google.android.exoplayer2. The names survived the move, which helps when you search a decompiled APK.
The interfaces ExoPlayer defines
Four types describe the API specification. DrmSessionManager decides whether a track can be played and hands out sessions. DrmSession represents one session with one DRM system and exposes its state, any error, and a crypto handle for the decoder. ExoMediaDrm wraps the platform API so an app or a test can substitute another implementation, and its default form calls into android.media.MediaDrm. MediaDrmCallback performs the network half: it accepts the opaque request bytes and returns the response bytes.
An app configures these on the player. The DRM configuration usually travels with the media item, carrying the scheme UUID and the licence URI, and the session manager is built with a callback. That configuration is not secret. The values worth tracing are the ones added at request time.
How a session is created
- The manifest parser reads protection information for each track and produces init data: the scheme UUID plus the PSSH box or equivalent header that identifies the content and its key IDs.
- A renderer asks the session manager for a session for its track, passing that init data and the MIME type.
- The manager checks whether a session already exists for the same init data. When the player is configured for multi-session operation, tracks can hold separate sessions; in single-session mode they share one. That choice changes how many licence requests appear during playback.
- The selected session calls openSession on the underlying MediaDrm instance. The call fails with a not-provisioned exception when the CDM has no device certificate, which sends the flow down the provisioning path.
- The session asks MediaDrm for a key request and passes the resulting bytes to the callback.
Where the licence request is assembled
The player does not build the licence request. MediaDrm.getKeyRequest produces it from the init data, the MIME type, the key type and any optional parameters the app supplies. The result is a byte array that ExoPlayer treats as opaque, returned with a request type that distinguishes an initial request, a renewal and a release. On Widevine that array is a binary message carrying the content identification and the device certificate the CDM holds. The player never inspects it, which is why a proxy log or a body dump shows binary rather than JSON.
The callback then decides how to deliver it. HttpMediaDrmCallback in the Media3 DRM module posts the bytes to the licence URI, applies request headers the app configured, and reads the response body back as a byte array. Some apps replace this callback to route the request through the same HTTP client the rest of the app uses, which pulls the licence call into the app's own signing, logging and retry code.
Installing the keys
The response bytes return to the session, which calls MediaDrm.provideKeyResponse. The CDM validates the licence, decrypts it with material only it holds, and stores the content keys against that session. A refusal on the server side surfaces as a denied-by-server error inside the session rather than as an HTTP status the app can read.
Keys do not travel upward into the player. The renderer asks the session for a crypto handle and passes it to MediaCodec when configuring a secure decoder. The decoder obtains keys from the CDM, and the app holds a reference rather than the key material. A dump of the Java heap does not produce content keys, which is worth knowing before spending a day on one.
Provisioning
A CDM needs a device certificate before it can produce usable key requests. When the certificate is missing, openSession fails and the session calls getProvisionRequest. The callback sends those bytes to a provisioning endpoint, the response goes to provideProvisionResponse, and the CDM stores the certificate. Provisioning is per device and normally happens once, though it can repeat after a data wipe or a CDM reset. It also means a licence request captured on one device carries that device's certificate, which is one reason the same request can behave differently on another install.
What the architecture means for analysis
The MediaDrm boundary is the most direct place to observe a DRM flow. Hooking the key request and key response methods shows the exact CDM input and output. Hooking the callback or the data source shows the HTTP request as the app sent it, including the authorization the app wrapped around the blob. Session state and key status callbacks show renewals and expirations as they happen, so a flow that looks like a single request in a proxy log is often several requests across the life of a playback session.
What you can rebuild from those observations is the app's side of the exchange: the endpoint, the headers, the token, the session identity and the order of calls. The cryptographic side stays inside the CDM and is not recoverable from a hook that returns an opaque blob.
Related work
Reviewed 28 September 2026 · SReverse research desk