JavaScript Encrypted Media Extensions Table

PieceWhat it doesField note
requestMediaKeySystemAccess(ks, configs)The capability probeconjunctive and unforgiving: every declared capability must hold, no downward negotiation - ladder your configs strict to permissive
createMediaKeys() + setMediaKeysThe attachbefore feeding encrypted media; one MediaKeys on the element covers all sessions created from it
createSession(type)The license containertemporary for streaming, persistent-license for offline where allowed; most production is temporary
session message + update()The relayopaque license request out to your server, opaque response back via update - JavaScript is the courier, never reads keys
the encrypted eventThe bindinginitDataType + initData from the video element go to generateRequest - with MSE it fires when encrypted init segments land
robustness levelsThe protection ladderHW_SECURE_ALL down to SW_SECURE_CRYPTO - asking above the device fails the whole config silently
license renewalThe midstream hitchshort licenses fire more messages mid-stream - the relay must live for the whole stream or playback halts at renewal
the CDMThe black boxWidevine/PlayReady/FairPlay per platform, provisioned by the browser; debugging is player+server logs, not console.log
Reference: the MDN Encrypted Media Extensions. The standard courier route for protected video: negotiate capabilities, attach MediaKeys, relay opaque license messages - keys and decrypted frames never touch JavaScript.
Bottom line: the config is a capability expression language - build a ladder from strict to permissive and take the first access that resolves. Your server integration is one message listener that must stay alive for renewals. Black screens have signatures: rejected config, late setMediaKeys, license server errors, rejected update - log every relay step and reproduce on the target device, where older CDMs live. Clear-key validates your plumbing minus the real DRM.
Related tools: the media source table (the buffer pipeline DRM rides on), the fetch table (the license relay), the WebCrypto table (key material elsewhere in the stack), the service worker table (caching under licenses), and the postMessage table (license proxies across iframes).

Encrypted Media Extensions (EME) is the browser API for playing protected video: it exposes a standard way for the page to hand encrypted media to a Content Decryption Module (CDM) - the DRM implementation (Widevine, PlayReady, FairPlay) - and exchange license messages with your license server, without ever exposing keys or decrypted content to JavaScript. The name is honest: it is an extension shaped for encrypted media, and the encryption itself, the key exchange and the renewal policy all live outside the API in your server and the CDM.

Bottom line: the flow is a config negotiation followed by a message relay. navigator.requestMediaKeySystemAccess(keySystem, configs) asks whether this browser-CDM pair supports your codec, encryption scheme and robustness requirements; if it does, you create MediaKeys, attach them to your video (or your MediaSource buffers), create a MediaKeySession, feed it initData from the encrypted media, and relay the session's generated license request to your server and the response back via session.update(). JavaScript is the courier: it sees opaque messages, never keys.

The integration reality: EME almost always rides on MSE - the encrypted segments come through your SourceBuffer pipeline (the encrypted event fires on the video, carrying initData), and the DRM choice is not yours alone: Widevine dominates Chrome/Android, PlayReady owns Edge and living-room devices, FairPlay owns Apple. Production players query all key systems with isTypeSupported-style configs and pick the best available - which is why the config structure (initDataTypes, videoCapabilities, robustness) is the hardest part of the API surface: it is a capability expression language, not a function call.

How to use

  1. Probe the capability: navigator.requestMediaKeySystemAccess('com.widevine.alpha', config) - the config declares initDataTypes (usually 'cenc'), codec capabilities and videoCapabilities with robustness levels; it returns a promise of a MediaKeySystemAccess or rejects.
  2. Create and attach: const keys = await access.createMediaKeys(); await video.setMediaKeys(keys); - do this before feeding encrypted media to the element or its MSE buffers; setMediaKeys on the element covers all sessions you create from these MediaKeys.
  3. Open a session: const session = keys.createSession('temporary'); - temporary (streaming, gone on close), persistent-license (offline rentals, where allowed) or persistent-usage; most streaming is temporary.
  4. Relay the handshake: session.addEventListener('message', async e => { const resp = await fetchLicense(e.message); await session.update(resp); }); - the message is the opaque license request (type 'license-request'), your server answers, update() feeds it back; this one listener is the whole server integration.
  5. Bind the media: listen for the video element's encrypted event - event.initDataType + event.initData go to session.generateRequest(initDataType, initData) - with MSE the event fires when encrypted init segments hit the SourceBuffer; from there the element plays when keys arrive.

Frequently asked questions

What is a CDM, and why does EME never expose keys to JavaScript?

The Content Decryption Module is the component that actually holds keys and decrypts frames, and its independence from JavaScript is the design. The browser ships or provisions CDMs (Widevine is downloaded/provisioned by Chrome; PlayReady is baked into Windows; FairPlay into Apple systems), and the API contract deliberately routes everything through opaque blobs: session.message is bytes your code must relay but cannot read, license responses are bytes the CDM interprets, and decrypted frames go to the compositor, never to a canvas or a callback. The reasons are contractual as much as technical - studios license content against the assurance that keys never touch web-exposed memory - and they explain the API shape you otherwise would not design: generateRequest and update take opaque data, errors are deliberately vague, and robustness levels exist to promise hardware-level key protection. The practical consequence for you: nothing about EME is testable with console.log - integration debugging happens in the player logs, the license server logs and the CDM's own diagnostics, so build the logging seams on day one.

Why did requestMediaKeySystemAccess reject my config when the browser supports that DRM?

The config language is conjunctive and unforgiving: every capability you declare must hold simultaneously, and the browser rejects the whole config rather than negotiating downward. The classic mistakes: robustness strings that are too strict - asking for HW_SECURE_ALL when the device only honors SW_SECURE_CRYPTO fails the video capability outright (declare multiple configs in the array, from strict to permissive, and take the first access that resolves). initDataTypes mismatches - declaring 'keyids' when your media carries 'cenc' init data fails even though the DRM works. Codec-in-container mismatches - the videoCapabilities content type must match what your MSE SourceBuffers will actually feed, including profile levels. And key system names that vary by platform - com.widevine.alpha, com.microsoft.playready.recommendation vs the older com.microsoft.playready, com.apple.fps.2_0 vs 1_0 - the string is the contract, and typos or version guesses fail silently (a rejected promise). The professional pattern: a capability ladder (robust DRMs first, permissive fallbacks last), one probe function over the ladder, and a cached result per page life - the negotiation is expensive and its answer does not change mid-session.

How do sessions, licenses and renewal actually behave over a long stream?

A MediaKeySession is the stateful container for one license: created, fed a generateRequest, updated with the license response, and then usable until it closes, expires or the page kills it. The behaviors that bite long streams. License renewal: some licenses carry an expiry shorter than your stream - the CDM fires another message event (messageType 'license-renewal' where supported) and your same relay listener must answer it or playback halts mid-movie; the relay must be alive for the stream duration, not just the handshake. Heartbeats and release: persistent-usage and persistent-license sessions may require session.release() or remove() to tell the server the device stopped using the license - skipping it leaks concurrent-stream slots, which is how users hit the device-count wall. Multi-key content: one session can carry keys for multiple tracks (audio plus video keys in one license) or you can run separate sessions per key - pick per your licensor's packaging, and know that key rotation mid-stream (segments encrypted under a new key id) rides the same encrypted/generateRequest cycle transparently when init data changes. And closing: session.close() plus video.setMediaKeys(null) on teardown - otherwise the CDM holds the license (and the playback slot) until the browser reaps the page.

What are the standard failure modes, and how do I debug a black screen?

The black screen with a silent console is the genre-defining EME bug, and each mode has a signature. No key system access: the requestMediaKeySystemAccess promise rejected - your config asked for something the platform lacks (usually robustness); log the rejection reason per config rung. MediaKeys never attached: setMediaKeys failed or ran after the encrypted event fired - the element drops encrypted frames with no keys; sequence the attach before feeding MSE. License server 4xx/5xx: the fetch in your message relay failed and session.update never happened - the CDM sits waiting forever; log the server response status AND body before update, because license servers encode their reasons (entitlement expired, device limit, geo) in error bodies. update() rejection: the license was malformed, expired or for the wrong session - log the exception; the CDM message is your only diagnostic. Then the two meta-rules: every step of the relay (message out, response in, update accepted) gets a log line with the session id, and reproduce on the target device stack, because the CDM is per-platform - a config that sails through desktop Chrome fails on Android TV's older Widevine, and the smart-TV browser is where these bugs go to live. If you control the stack, test with clear-key (a built-in unencrypted-key CDM every browser ships for exactly this purpose): it validates your entire EME plumbing minus the real DRM, separating your bugs from the vendor's.

Related tools