---
status: draft
date: 2026-05-20
definition: coarse
---

# DRM support

Digital Rights Management for protected content via the W3C
Encrypted Media Extensions (EME) specification. Covers MediaKeys /
MediaKeySession lifecycle, license-server interaction, key delivery,
and the per-key-system specifics for Widevine, PlayReady, and
FairPlay. The cluster F foundation that consumer-facing protected-
playback features (e.g., a Mux Player `drm-token` integration) build
on.

A **Media-src feature** in the framing from
[clusters.md § Feature classification axes](./clusters.md#feature-classification-axes):
without it, DRM-protected sources don't play. The engine has to set
up MediaKeys, negotiate a MediaKeySession, fetch and respond to
licenses, and deliver keys before the browser will decrypt segments.
*Which* key systems are supported is owned by
[capability-probing](./capability-probing.md) (cluster D); *setting
up* the chosen key system is owned by this feature.

Tracked via **[GitHub issue #1411](https://github.com/videojs/v10/issues/1411)**
("Feature: DRM Support") with three per-key-system sub-issues:
[#1412 Widevine](https://github.com/videojs/v10/issues/1412),
[#1413 PlayReady](https://github.com/videojs/v10/issues/1413),
[#1414 FairPlay](https://github.com/videojs/v10/issues/1414).
Milestone: GA. Prior art: [videojs-contrib-eme](https://github.com/videojs/videojs-contrib-eme)
(Video.js v8 plugin), [Mux Player DRM integration](https://www.mux.com/docs/guides/protect-videos-with-drm)
(Widevine + PlayReady + FairPlay via `drm-token` attribute).

## Status

- **Composition:** not implemented in `createSimpleHlsEngine`. No
  decryption pipeline: nothing sets up MediaKeys, negotiates a session,
  or requests a license. What exists is the *refusal* —
  `parseMediaPlaylist` recognizes `#EXT-X-KEY` well enough to flag a
  rendition `encrypted`, `canPlayTrack` prunes those renditions, and a
  fully-encrypted type reports `SVTA_UNSUPPORTED_DRM_SYSTEM` ("this
  video is protected") rather than a generic unsupported-format error.
  `#EXT-X-SESSION-KEY` is still unrecognized, which matters: Mux emits
  no session-key tag in the multivariant playlist, so encryption is
  undiscoverable until a media playlist is fetched — hence pruning at
  resolve time rather than at manifest parse.
- **Definition depth:** coarse — scope identified from GitHub issue
  + prior art; SPF touchpoints sketched at the cluster level; no
  implementation. The three sub-issues each carry per-key-system
  detail.
- **Hard prerequisite:** [capability-probing](./capability-probing.md)'s
  "Key-system capability probing" phase. The probe must resolve
  before this feature commits to a key system, sets up MediaKeys,
  or fetches a license. Crisp boundary per
  [clusters.md § Encrypted media (DRM)](./clusters.md#encrypted-media-drm):
  probing answers "can we?"; this feature answers "set it up."

## Phases of complexity

Scope slices around the implementation layers. Per-key-system
specifics are listed within a single phase row that points at the
three sub-issues, rather than as separate phases.

| Phase | What | Notes |
|---|---|---|
| EME setup pipeline | Capability-probing's key-system verdict drives `navigator.requestMediaKeySystemAccess(...)`, which produces a `MediaKeys` instance. `mediaElement.setMediaKeys(mediaKeys)` is called **before MediaSource attachment** per W3C EME (setMediaKeys must complete before encrypted content appends; pre-attachment is the spec-safe ordering). `encrypted` event on the media element fires when init data is appended → `MediaKeySession.generateRequest(initDataType, initData)` starts a session | Shared infrastructure regardless of key system. The generic ordering is settled by spec; the SPF composition question is how to express the gate without DRM-awareness leaking into the standard `setupMediaSource` behavior — see Likely cross-cutting impact for the variant-composition framing |
| License flow | Per-source license-server configuration (consumer-provided URL + optional headers / auth tokens). `MediaKeySession.message` event → fetch license from server → `MediaKeySession.update(licenseResponse)`. Pluggable hooks for consumer policies (custom headers, body transformation, response transformation) | Consumer-facing config surface lives here. Mux Player's `drm-token` attribute is one consumer of this; videojs-contrib-eme's per-key-system config is another precedent |
| Per-key-system specifics | Widevine ([#1412](https://github.com/videojs/v10/issues/1412)), PlayReady ([#1413](https://github.com/videojs/v10/issues/1413)), FairPlay ([#1414](https://github.com/videojs/v10/issues/1414)). Per-system: init-data format (PSSH for Widevine, PRO box for PlayReady, content-id derivation for FairPlay), license URL conventions, license body format, server-certificate handshake (FairPlay), browser-API quirks. **FairPlay-AirPlay is a distinct key system from standard FairPlay** (see [capability-probing](./capability-probing.md)'s four-key-system enumeration) — active when content streams via AirPlay; entering/exiting AirPlay mid-playback is a *runtime state change*, not a compose-time variant, raising an open question on runtime-switching shape (see Open questions) | Three sub-issues. The shared pipeline + license flow above handle most of the machinery; each key system adds its own init-data + license-format adapters. FairPlay-AirPlay sits as a runtime-switchable variant of FairPlay specifically |
| Key delivery and `keystatuschange` reactivity | Browser receives keys via `MediaKeySession.update()`; encrypted segments decrypt automatically. `MediaKeySession.keystatuses` Map tracks per-key status (`usable`, `expired`, `output-restricted`, `released`, etc.); `keystatuschange` event fires on changes. Engine reacts to status transitions (e.g., expired key → re-request) | Tier 2-ish: engine can ignore non-`usable` statuses initially (key expiry surfaces as a playback failure); richer handling is consumer-policy-driven |
| Security-level capability and constraint filtering | Probe device security level (Widevine L1 hardware-backed / L2 hybrid / L3 software-only; PlayReady SL150 / SL2000 / SL3000; FairPlay key-duration / persistent-vs-streaming model) via `MediaKeySystemAccess.getConfiguration()`. HDCP output-protection requirements similarly probed. Match against per-rendition security-level requirements (e.g., 4K HDR HEVC often requires L1 Widevine) and license-server policy. Write a `deviceSecurityLevel` constraint slot read by ABR / variant selection; renditions exceeding the device's level filter out, or the engine surfaces a failure when no compatible rendition remains | Constraint+filter pattern parallel to [rendition-selection-caps](./rendition-selection-caps.md) and [hevc-variant-selection](./hevc-variant-selection.md). Probing extends [capability-probing](./capability-probing.md)'s key-system probe with security-level configuration. Borderline classification (Media-src for "play protected content correctly"; Player for customer-policy caps) — current scope leans Media-src |
| Parser surface for key tags | `parseMediaPlaylist` recognizes `#EXT-X-KEY` and `#EXT-X-SESSION-KEY` from media playlists; multivariant parser surfaces session keys at presentation resolution. Init-data flows from the parsed-track output through MSE setup to the EME pipeline | Parser-side change. Today neither tag is recognized; both are silently passed through |
| Encrypted-event handling on `SourceBuffer` / mediaElement | `encrypted` event on `mediaElement` triggers session creation via init-data. Once keys are delivered, segment-append proceeds normally; the engine doesn't intervene per-segment | Cross-cluster MSE concern; segment-append flow is unchanged for encrypted streams aside from the key-readiness gate |

## What's in scope vs out of scope

**In scope:**
- All seven phases above for HLS protected content with EME-supported
  key systems (Widevine, PlayReady, FairPlay)
- MediaKeys / MediaKeySession lifecycle management (per-source setup,
  source-change cleanup)
- License fetcher with consumer-pluggable URL / headers / body
  transformation hooks
- Parser surface for `#EXT-X-KEY` and `#EXT-X-SESSION-KEY` tags
- `keystatuschange` event reactivity baseline (surface failures)
- Engine-composition variant for DRM-required content
- Security-level probing extension to capability-probing's key-system
  probe (returns supported security levels per key system via
  `MediaKeySystemAccess.getConfiguration()`)
- Security-level constraint slot (`deviceSecurityLevel`) + filter-
  pattern integration with rendition selection (parallel to
  `userVideoTrackSelection` in video-abr.md and per-cap slots in
  rendition-selection-caps.md)
- HDCP output-protection requirement detection and gating

**Out of scope (separate Media-src candidate features):**
- **Key-system capability probing** — owned by
  [capability-probing](./capability-probing.md). Crisp boundary:
  probing answers "can we?"; this feature answers "set it up."

**Out of scope (different architectural layer):**
- Adapter-layer customer-facing API surfaces (e.g., a Mux Player
  `drm-token` attribute, a consumer-passed license-server URL config).
  The SPF feature owns the engine-side license fetcher with pluggable
  hooks; the adapter / consumer provides the actual URL, headers,
  authentication tokens. videojs-contrib-eme's `keySystems` config
  shape is one reference for the consumer surface.
- License-server hosting and DRM token signing. Service-side concerns.
- Per-device key-system installation, content-decryption-module
  updates. Browser / OS responsibilities.
- DRM error UX (license expired, output not permitted) above the
  engine. Engine surfaces failures; adapter renders UX.

## Likely cross-cutting impact

Things this feature probably forces decisions on, not just additions:

- **Gate chain extension on MSE setup — composition-variant placement.**
  Per W3C EME, `mediaElement.setMediaKeys()` must complete before
  encrypted content is appended to a SourceBuffer; the spec-safe
  ordering is pre-MediaSource-attachment. Today's MSE gates per
  [mse-mms-pipeline.md](./mse-mms-pipeline.md): MediaElement +
  presentation URL + `'open'` readyState gate `setupMediaSource`;
  resolved track + open MediaSource gate `setupSourceBuffers`.
  Adding a "MediaKeys attached" gate between these is straightforward
  in isolation; the SPF composition question is how to do it
  *without* DRM-awareness leaking into the standard `setupMediaSource`
  (which today, and in DRM-free engine variants, has no need to know
  about MediaKeys). Per the failure-mode catalog's composition-
  variant entry: variant-specific behaviors compose into DRM-required
  engine variants, not as runtime branches in always-on behaviors.
  Two likely shapes:
  - **(a)** DRM-required engine variant composes a *different*
    `setupMediaSource` that gates on a `mediaKeysReady` signal before
    attaching; standard `setupMediaSource` composes into non-DRM
    engines unchanged.
  - **(b)** A new `setupMediaKeys` behavior writes to a generic
    "ready-to-attach" gate slot that `setupMediaSource` reads;
    standard engines provide a default-true writer for the slot.
  Option (a) is cleaner composition-variant discipline (DRM-free
  engine's `setupMediaSource` is genuinely unchanged); option (b)
  introduces a slot whose primary purpose is DRM. Lean: (a).
  Beyond setMediaKeys, additional DRM gates fire downstream:
  capability-probing's key-system verdict (Tier 1 gate, fires once
  per source); per-session license obtained + `keystatuschange`
  confirms at least one `usable` key (fires on key delivery,
  decryption is async beyond this gate).
- **Composition variant for DRM-required content.** DRM-required
  engine variants compose additional behaviors (MediaKeys setup,
  license fetcher, encrypted-event handler) atop the standard
  composition; DRM-free engines don't carry the machinery. Same
  shape as the live / DVR / LL-HLS variant pattern. The decision
  point — does the consumer opt into a DRM variant upfront, or does
  the engine detect DRM from `#EXT-X-KEY` / `#EXT-X-SESSION-KEY`
  parser output and route accordingly — is open. Adapter-upfront
  is simpler; detect-and-route is more adaptive.
- **State slots for DRM lifecycle.** New slots likely include:
  `drmReady` (key system probed + MediaKeys attached + license
  obtained), `mediaKeysReady` (intermediate gate), and possibly
  per-session state for license-renewal scenarios. Multi-writer
  characterization is open until the slot family solidifies.
- **Encrypted-segment buffer behavior.** Once keys are delivered,
  the MSE pipeline appends encrypted segments unchanged. The
  encrypted-event flow happens *before* steady-state appending. No
  per-segment decrypt overhead from the engine's perspective; the
  browser handles decryption transparently. The MSE codec-change
  check does not fire — DRM doesn't change codec.
- **Source-replacement cascade under DRM.** When the consumer
  changes `presentation.url`, the existing MediaKeys / MediaKeySession
  tear down; the new source's DRM setup runs fresh. Standard
  resolved/unresolved cascade per
  [source-replacement.md](./source-replacement.md), with MediaKeys
  cleanup as an additional in-place cleanup target.
- **License-fetcher composability.** License fetching is the most
  consumer-customizable surface (custom URLs per content, custom
  headers / auth tokens per session, custom body transformations for
  Mux's DRM token format vs raw EME, etc.). The engine should expose
  pluggable hooks rather than hardcoded behavior. videojs-contrib-eme's
  `keySystems.*.getLicense()` callback is one shape; a more general
  request/response-transformation interface is another.
- **Per-key-system browser-API differences.** Widevine, PlayReady,
  FairPlay all have spec-compliant EME surfaces, but the
  init-data formats, license-message formats, server-certificate
  handshakes, and key-status semantics differ. Per-key-system adapter
  modules (one per sub-issue) handle the system-specific logic; the
  shared EME pipeline calls into them via a uniform interface.

## Open questions

- **Variant-decision signal source.** Adapter-upfront opt-in (consumer
  knows the source is DRM-protected, instantiates a DRM-capable
  engine variant) vs detect-from-parser (engine sees `#EXT-X-KEY` and
  routes to DRM-capable composition). Adapter-upfront is simpler;
  detect-and-route is more adaptive but adds composition-time-
  decision complexity.
- **Composition-variant shape for the setMediaKeys gate.** Per the
  cross-cutting note: variant-specific `setupMediaSource` (option a)
  vs generic ready-to-attach slot (option b). The spec-required
  ordering (`setMediaKeys` before MediaSource attachment) is not in
  question; only the SPF composition shape is. Resolving this
  constrains how DRM-required engine variants are composed and
  whether non-DRM engines remain genuinely unchanged. Lean: (a) for
  composition-variant discipline.
- **FairPlay-AirPlay runtime switching.** AirPlay session state is
  a *runtime* condition (user can enter/exit AirPlay during
  playback), not a compose-time variant. This breaks the standard
  composition-variant discipline (compose-time variants for compose-
  time conditions). Two open shapes: (a) a middle-pattern behavior
  monitors AirPlay session state (`mediaElement.remote.state` or
  equivalent) and writes an `airplaySessionActive` slot; a DRM-variant
  behavior reads the slot and reacts (re-request key system,
  re-create MediaKeys, potentially flush buffer + re-fetch license).
  MediaKeys-recreation mid-source is non-trivial — the standard
  source-replacement cascade tears MediaKeys down on
  `presentation.url` change; an AirPlay-triggered recreation would
  need a narrower reset (MediaKeys only, not the rest of the source
  state). (b) Defer the switch entirely: standard FairPlay key system
  is used for both local and AirPlay playback, accepting any degraded
  behavior or playback errors during AirPlay sessions. Likely (a)
  but the implementation is substantial; this open question may
  itself motivate a follow-on feature doc once FairPlay implementation
  ([#1414](https://github.com/videojs/v10/issues/1414)) lands.
- **License-fetcher hook shape.** Single `getLicense(message)` callback
  vs separate request-transformation + response-transformation hooks
  vs a full fetch-wrapper interface. videojs-contrib-eme's shape vs
  more general request/response interceptors. Consumer-policy
  flexibility vs engine-side complexity trade-off.
- **MediaKeys re-use across sources.** When the consumer changes
  sources within the same key system + license server, should the
  engine re-use the existing MediaKeys instance or tear down and
  recreate? Re-use saves the `requestMediaKeySystemAccess` cost but
  complicates lifecycle. videojs-contrib-eme tears down per-source;
  worth verifying whether SPF wants the same default.
- **`keystatuschange` reactivity policy.** Engine-baseline behavior on
  key-status transitions (expired, output-restricted, released, etc.).
  Surface as a state-error slot? Trigger automatic re-request? Defer
  to consumer? Defaulting matters because keystatus changes can fire
  mid-playback.
- **Init-data extraction location.** Parser-side (extract during
  `parseMediaPlaylist`) vs segment-side (extract from segment init
  data via `encrypted` event). Both are valid; parser-side is more
  predictable, segment-side is more reactive. May not be either-or
  per the spec.
- **Cross-feature: DRM + live / DRM + DVR.** Live and DVR streams
  with DRM are valid use cases. The reload-loop interacts with key
  renewal cadence (if licenses expire during a long live session,
  the reload-loop + license-fetcher may both need re-trigger logic).
  Cross-cluster A + F open question; resolution likely after both
  clusters have implementation work.
- **Output-protection-aware ABR coordination.** Renditions tagged
  with security-level / HDCP requirements interact with video-ABR
  and hevc-variant-selection. ABR's candidate set should be filtered
  by the `deviceSecurityLevel` constraint slot before bandwidth-
  driven selection runs. Filter ordering: capability filter
  (physics) → policy caps → security-level → bandwidth-driven
  selection. Per-rendition requirement tagging is open: parser-
  surfaced HLS extension attributes vs runtime-probed via license-
  server policy vs both. Server-side conventions vary.
- **Per-rendition security-level tag surface.** HLS doesn't have a
  spec-defined attribute for "this rendition requires L1 Widevine."
  Providers commonly encode the requirement in proprietary attributes
  (`URI-SECURITY-LEVEL`, etc.) or imply it from `RESOLUTION` thresholds
  (4K+ requires hardware DRM by convention on many platforms). Parser
  needs an extension axis for surfacing the requirement; license-
  server policy is the orthogonal source.

## Related features

- **[capability-probing](./capability-probing.md)** *(hard
  prerequisite)* — owns key-system probing; this feature consumes the
  verdict. Crisp boundary: probing = "can we?"; this feature =
  "set it up."
- **`[fairplay-airplay-workaround]`** *(candidate, this session)* —
  Apple-specific FairPlay quirks during AirPlay sessions. Consumes
  this feature's FairPlay setup as the baseline.
- **[mse-mms-pipeline](./mse-mms-pipeline.md)** — DRM gates MSE
  setup; encrypted-event flow on `mediaElement` triggers session
  creation. Once keys are delivered, segment append proceeds
  unchanged.
- **[capability-probing](./capability-probing.md)** — also relevant
  for `changeType()` probing if mid-stream codec changes interact
  with DRM (uncommon but possible).
- **[source-replacement](./source-replacement.md)** — MediaKeys /
  MediaKeySession cleanup on source change. Standard resolved/
  unresolved cascade with DRM additions.
- **[live-stream-support](./live-stream-support.md)** *(not yet
  implemented)* — DRM + live combines naturally; license-renewal
  cadence during long live sessions is the open question.
- **[dvr-event-stream-support](./dvr-event-stream-support.md)** *(not
  yet implemented)* — DRM + DVR with back-seek through history; key
  delivery for back-seek-fetched segments needs verification.
- **[video-abr](./video-abr.md)** / **[hevc-variant-selection](./hevc-variant-selection.md)**
  — output-protection-aware variant filtering when `drm-security-
  levels` lands.

## See also

- [GitHub issue #1411 — Feature: DRM Support](https://github.com/videojs/v10/issues/1411)
  — the tracking epic; per-key-system sub-issues [#1412 Widevine](https://github.com/videojs/v10/issues/1412),
  [#1413 PlayReady](https://github.com/videojs/v10/issues/1413),
  [#1414 FairPlay](https://github.com/videojs/v10/issues/1414)
- [clusters.md § Encrypted media (DRM)](./clusters.md#encrypted-media-drm)
  — cluster F description; this feature is the foundation
- [clusters.md § Capability probing](./clusters.md#capability-probing)
  — cluster D; the probing prerequisite this feature consumes
- [clusters.md § Feature classification axes](./clusters.md#feature-classification-axes)
  — Media-src feature framing
- [capability-probing.md](./capability-probing.md) — hard
  prerequisite; key-system probing
- [videojs-contrib-eme](https://github.com/videojs/videojs-contrib-eme)
  — Video.js v8 prior art; key-system detection in
  [`src/cdm.js`](https://github.com/videojs/videojs-contrib-eme/blob/main/src/cdm.js)
- [Mux Player DRM integration](https://www.mux.com/docs/guides/protect-videos-with-drm)
  — adapter-layer prior art; `drm-token` attribute on `<mux-player>`
- [W3C Encrypted Media Extensions](https://www.w3.org/TR/encrypted-media/)
  — EME spec; [`MediaKeySystemAccess.getConfiguration()`](https://www.w3.org/TR/encrypted-media/#dom-mediakeysystemaccess-getconfiguration)
  is the security-level probing surface
- [HDCP specification (DCP LLC)](https://www.digital-cp.com/) —
  output-protection requirements; the protection-level data this
  feature gates on for high-resolution / premium content
- [HLS Spec — `EXT-X-KEY` / `EXT-X-SESSION-KEY`](https://datatracker.ietf.org/doc/html/rfc8216bis)
