Skip to content

Feature: On-Demand Key Licensing and Session Sharing Across Keys #2863

Description

@cjpillsbury

Context

exchangeLicenses opens one MediaKeySession per declared key, eagerly, at entry — including renditions never played. Studio-policy content commonly declares a different key per quality tier, so a three-tier ladder fires three licence requests at startup. Providers enforce concurrent-session limits and some bill per licence issued.

The behaviour is pinned by a test (one session per distinct key, selection never consulted) and observable against a real server via the hls-drm-axinom-multikey sandbox vector, where three startup POSTs are visible.

Three independent axes

Worth separating, because the obvious framing ("eager vs on-demand") hides one:

  1. When — license at entry, or on encounter.
  2. How many sessions — one per key, or reuse a session whose keyStatuses already covers the key.
  3. Which keys — every declared key, or only the selected renditions (the selected-track ids are already in state).

Axis 2 is largely independent and is the cheapest. EME does not require a session per key: MediaKeySession.keyStatuses is a map, and a Widevine or PlayReady licence routinely returns several keys. The fan-out here follows from deriving one init data per #EXT-X-KEY, not from a design decision.

Note that concatenating PSSH boxes is not the route. Per the "cenc" initialization data format, the init data is one or more concatenated pssh boxes each for a unique SystemID, and a CDM takes the first box it supports — so merging several Widevine PSSHs would have the CDM use only the first.

Prior art — hls.js already does axis 2

getSessionForKey → findContextForKey checks whether an existing session's keyStatuses already contains the key's keyId, and reuses that session when it does. Their own comment:

Waiters re-enter resolveSessionForKey after the predecessor settles so they can share sessions containing their keyId's key-status, or generate their own license request and key session if the license server does not produce multiple key statuses.

So they do not assume the server multi-keys — they observe whether it did. They also serialise concurrent requests per key URI so two callers cannot race into duplicate sessions for one URI (video-dev/hls.js#7796).

Shaka does not do this: it suppresses duplicate init data by byte comparison (ignoreDuplicateInitData) and otherwise creates a session per distinct init data, which is where we are today.

Acceptance criteria

  • Where a licence response covers several keys, subsequent keys reuse the session already holding them rather than opening a new one — and where it does not, behaviour is unchanged.
  • Concurrent requests for the same key URI cannot produce duplicate sessions.
  • Licensing is driven by what playback needs rather than by everything the manifest declares.
  • A rendition switch onto a tier whose key was not declared at entry opens a session for that key. This is the correctness half of this issue (see "Confirmed defect" below) and should land with or before the efficiency half.
  • The existing fan-out pin test is updated to encode the new contract, not deleted — it currently pins the behaviour being changed, which is the right place for the change to become visible.

Confirmed defect — per-tier keys are licensed only for the entry-time variant

Found by Cursor Bugbot on #2291 and confirmed by inspection 2026-09-17. This is not just an efficiency gap; on one asset shape it is a decode failure.

exchangeLicenses reads manifestInitData(presentation, …) once in its licensing entry. Three facts make that lossy:

  1. resolve-track gates on a selection — it resolves the selected rendition's media playlist and patches it back into state.presentation. Other variants stay unresolved.
  2. declaredDrmKeys counts keys on resolved tracks only (isResolvedTrack).
  3. The reactor cannot re-enter: derivedStateSignal returns 'licensing' for any resolved presentation, so a presentation that later gains a resolved variant produces no state transition and no second entry.

So a studio-policy ladder with a distinct KEYID per tier declares exactly one key at entry. An ABR switch across a key boundary then decrypts against a key with no session.

#EXT-X-SESSION-KEY, the standard way to declare every key up front, is deliberately not parsed on #2291 — so nothing masks this.

Why it was not caught:

  • Axinom's MultiKey vector plays because the smoke never left the initially-selected tier.
  • The fan-out pin test passes because it is handed a presentation with all four variants already resolved — a state the engine does not produce at entry.

Both remain valid tests of what they pin; neither covers this. A regression test wants a presentation that gains a resolved variant after licensing begins.

Reproduction — the Axinom MultiKey vector is a live repro

Measured from its manifests 2026-09-17 (hls-drm-axinom-multikey, media.axprod.net/TestVectors/MultiKey/Cmaf_h264_1080p_cbcs):

  • 5 variants, 2 distinct KEYIDs, split at the 480→720 boundary — 288/360/480 on 0xC83C4EA80F2A4523851CFBECCDC0F202, 720/1080 on 0xC868C702C71B4064AE2BC24F7CC10792
  • No EXT-X-SESSION-KEY
  • Each variant playlist declares only its own key, twice — once as a Widevine PSSH, once as an skd:// URI naming the same keyid

So: start on any SD tier, force a switch across 480→720 (or start on HD and switch down), and the new tier needs a key with no session. It plays today only because a smoke that stays on one side of the boundary never needs the other key.

This also corrects a claim previously carried in the sandbox source comment and the risk assessment — that this asset shows a license POST per ladder key at startup. It cannot: only the selected variant is resolved, so exactly one key is declared and one license fetched. Fixed in 3b9d010.

The fix is the same reactive re-scan of declaredDrmKeys that live rotation needs, which is why it belongs here rather than in its own issue.

Notes

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    mediaspfStream Processing Framework

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions