Day 3 · 28 min read

EME Workflow

Encrypted Media Extensions: from encrypted event to license.

What EME is

EME (Encrypted Media Extensions) is the W3C JavaScript API browsers expose for DRM-protected playback. It's deliberately key-system-agnostic: the same code drives Widevine, PlayReady, or FairPlay depending on what the browser supports.

EME deliberately offloads two things to the application:

  1. Network calls between the CDM and the licence server. The browser hands you opaque message bytes; you POST them; you feed the response back.
  2. Init data extraction, when needed. For CENC content, the browser extracts PSSH and gives you the init data for free.

This split is why EME integrations have so many places to go wrong — every byte that crosses the application is a place where wiring can break.

The complete flow

EME end-to-end
Step 1 of 9

ApprequestMediaKeySystemAccess('com.widevine.alpha', config)

The app describes the codecs and robustness it needs. The browser returns a MediaKeySystemAccess if the CDM supports the requested capabilities.

Code skeleton

The shape of the integration in real JavaScript:

async function play(video, src, licenseUrl) {
  // 1. Capability check
  const access = await navigator.requestMediaKeySystemAccess(
    "com.widevine.alpha",
    [{
      initDataTypes: ["cenc"],
      audioCapabilities: [{ contentType: 'audio/mp4; codecs="mp4a.40.2"' }],
      videoCapabilities: [{
        contentType: 'video/mp4; codecs="avc1.640028"',
        robustness: "SW_SECURE_CRYPTO", // L3-style; "HW_SECURE_ALL" for L1
      }],
    }]
  );

  // 2. MediaKeys + bind
  const mediaKeys = await access.createMediaKeys();
  await video.setMediaKeys(mediaKeys);

  // 3. Source
  video.src = src;

  // 4. Encrypted event drives session creation
  video.addEventListener("encrypted", async (e) => {
    const session = mediaKeys.createSession("temporary");
    session.addEventListener("message", async (event) => {
      const resp = await fetch(licenseUrl, {
        method: "POST",
        headers: { "Authorization": `Bearer ${appToken}` },
        body: event.message,
      });
      if (!resp.ok) throw new Error("licence failed");
      const buf = await resp.arrayBuffer();
      // 7. Feed back to CDM
      await session.update(buf);
    });
    // 4. Build the request
    await session.generateRequest(e.initDataType, e.initData);
  });

  await video.play();
}
Common pitfall

Forgetting to attach the message handler before calling generateRequest is a classic mistake — by the time you attach, the message has already fired into the void.

Session types

EME defines three session types, set when you call createSession(type):

TypeUse caseNotes
"temporary"VOD / live streamingDefault. Keys live in memory; closed when session is.
"persistent-license"Download-to-go offlineCDM persists the licence; load(sessionId) brings it back later.
"persistent-usage-record"Audit / consumption trackingPersists usage data, not the licence itself. Less common.

Robustness strings

The robustness field in videoCapabilities is how the application asks for a security level on Widevine:

  • "" (empty) — let the CDM decide.
  • "SW_SECURE_CRYPTO" — L3-equivalent: software CDM with software crypto.
  • "SW_SECURE_DECODE" — software CDM, decode also software, but secure path.
  • "HW_SECURE_CRYPTO" — keys in TEE; decode in rich OS (≈ L2).
  • "HW_SECURE_DECODE" — keys + decode in TEE for video (≈ L1 video path).
  • "HW_SECURE_ALL" — full L1 — keys, decode, all in TEE.

UHD policies typically demand HW_SECURE_ALL. If the device can't satisfy the requested robustness, requestMediaKeySystemAccess rejects.

Common error sites

Common pitfall

Mismatched messageType. When renewal arrives, the app must still POST it but may need to attach a different auth context. Branch on event.messageType.

Common pitfall

Calling setMediaKeys after video.src. Order matters: bind keys first, then attach source. Otherwise the encrypted event may fire before keys are bound and playback fails.

Common pitfall

Not handling 'waitingforkey'. This fires when a sample needs a key the CDM doesn't have — typical at key-rotation boundaries if renewal is broken. If you ignore it, the user sees an indefinite stall.

No questions yet for eme-workflow. Add some in content/questions/eme-workflow.json.