Trace the player's request chain
Media3 ExoPlayer uses Android's MediaDrm API for protected playback, but the app controls the network around it: entitlement, playback token, manifest URL, DRM configuration, license URL and request headers. Those calls explain most failures before a license reaches the device DRM module, so they are where the reconstruction work sits.
Find the DRM configuration
Find where the app builds its MediaItem or DrmConfiguration and record the scheme UUID, the manifest URI, the license URI, the multi-session setting and the request properties. A custom DrmSessionManager or callback can replace the default request handling, so follow those classes when the traffic differs from a standard ExoPlayer setup.
jadx -d out app.apk grep -rloE 'MediaDrm|DrmConfiguration|MediaItem.Builder|HttpMediaDrmCallback' out/sources | sort -u
The manifest carries protection-system data. Widevine uses the scheme UUID edef8ba9-79d6-4ace-a3c8-27dcd51d21ed, and the license request passes through a partner-operated proxy per Google's Widevine architecture. That means the license URL is a proxy, not Google's service directly, and the proxy applies the operator's business rules before the request reaches Widevine.
Capture the license exchange
Hook MediaDrm to see the request and response at the boundary. getKeyRequest returns the PSSH plus the configuration the app posts to the license URI, and provideKeyResponse returns the device-bound license. Observe the HTTP exchange over the proxy as well; the two together separate a network failure from a DRM failure.
Java.perform(function () {
var M = Java.use('android.media.MediaDrm');
M.getKeyRequest.implementation = function (sessionId, data, mimeType, keyType, opts) {
var out = this.getKeyRequest(sessionId, data, mimeType, keyType, opts);
console.log('key request type', keyType, 'mime', mimeType);
return out;
};
});Separate server state from device DRM
- Entitlement confirms the account can play the title.
- Playback APIs return a manifest URL and short-lived tokens.
- The manifest describes the streams and the protection metadata.
- The DRM client creates a key request from that metadata.
- The license proxy checks policy and returns a device-bound response.
Each step is a separate network call, and each one can fail for a different reason. Reconstruct the whole chain in order, because a failure in entitlement means the client never reaches the manifest, and a failure in the manifest means the client never builds a key request.
Choose the next layer by signal
| Signal | What it points to | Next check |
|---|---|---|
| 401 or 403 at entitlement or license proxy | Account state, token, cookie or request signing. | Trace the entitlement and token flow, then the request headers. |
| A Media3 DRM error after a good HTTP response | Scheme, session, policy or protection data. | Check the pssh, the scheme UUID and the supported security level. |
| No key request sent | The manifest did not reach the DRM client. | Confirm the manifest and any proxy that edits it. |
| License denied on your build, works in the app | Device-bound key or a proxy policy. | Check the security level and what the query proves. |
The device-bound constraint
Widevine security levels matter. A software (L3) license can often be reproduced outside the device; a hardware (L1) key is device-bound and CopyProtection media can block capture and screen recording. That is a deployment requirement, not a reversing problem. A ported client cannot produce an L1 license without the device key, so the analysis has to state which level the content requires before promising a server-side reproduction.
Read the pssh and the license response
The manifest carries the protection metadata. For a DASH stream that is a pssh box inside an init segment; for an HLS stream it is a key attribute in the playlist. Extract it and read the scheme it names, because that tells you which DRM system the stream uses before you look at the app.
# find the pssh box in an init segment or a plain stream grep -aob 'pssh' sample.mp4 | head xxd -l 48 sample.mp4 | head # and confirm the scheme UUID that the box carries
The key request the app posts is built from that pssh plus a device-wide session. The license that comes back is bound to that device and session, not to the pssh alone. So a license you captured from one device will not validate on another, and that is expected rather than a fault in your reconstruction.
When the app builds a second request with the same pssh and the server rejects it, compare the session data and the policy returned in the first response. The pssh is a constant for a given title; the session and the key are not.
Reviewed 30 August 2026 · SReverse research desk