DRM
mattebox@0.12.1
This chapter covers protected content through Encrypted Media Extensions.
EME stages
| Stage | Provides |
|---|---|
eme-core |
The EME handshake, session management, and ClearKey |
eme-cenc |
Widevine and PlayReady over Common Encryption |
eme-fairplay |
FairPlay Streaming on Safari |
eme-core is required by the other two. Load the key systems your content
uses.
import emeCenc from 'mattebox/stages/eme-cenc';
import emeCore from 'mattebox/stages/eme-core';
import emeFairplay from 'mattebox/stages/eme-fairplay';
const engine = mattebox({
stages: [
hlsCmaf(),
dashCmaf(),
emeCore({ licenseUrl: 'https://license.example.com/widevine' }),
emeCenc(),
emeFairplay({ certificateUrl: 'https://license.example.com/fairplay.cer' }),
],
});
Init data
Init data comes from the manifest (EXT-X-KEY, ContentProtection) or from
the media, through the element’s encrypted event. eme-core takes both
and opens one session per key id, whichever arrives first.
The protocol adapters always parse protection info, so adding DRM later changes nothing there.
License servers
| Option | Meaning |
|---|---|
licenseUrl |
One server for every key system |
certificateUrl |
The FairPlay certificate, when the eme-fairplay stage has none |
licenseUrls |
A server per key system, keyed by name such as com.widevine.alpha |
requestFilter |
Rewrites the license request body, for auth tokens or wrapping |
preferredKeySystems |
The order to try when the content offers several |
clearKeys |
Key id to key, base64url, for ClearKey |
releaseOnSuspend |
Closes the key sessions on suspend and licenses them again on resume |
emeCore({
licenseUrls: {
'com.widevine.alpha': 'https://license.example.com/widevine',
'com.microsoft.playready': 'https://license.example.com/playready',
'com.apple.fps': 'https://license.example.com/fairplay',
},
requestFilter: (body, keySystem) => body,
});
The URL can change at runtime.
engine.drm.setLicenseUrl('https://license.example.com/widevine?token=abc');
License requests go through the transport, so the request hooks from chapter 11 apply to them.
License renewal
eme-core renews a license when a key expires. It opens a new session with
the same init data, sends a new license request, and closes the old session
once the new key is usable. Playback continues when the new key arrives.
- Renewal waits for playback. A key that expires while the element is paused
or the engine is suspended renews on the next
playevent. - A renewal message the CDM sends on its own, such as Widevine’s
license-renewal, goes to the same license server. - A license that arrives already expired reports
DRM_KEY_EXPIRED. The engine does not request it again.
Release on suspend
A rights server that limits concurrent streams counts a session as long as
it holds a license and renews it. With releaseOnSuspend: true,
engine.suspend() closes every key session and emits drm:released with
the number of sessions. engine.resume() requests the same licenses again.
emeCore({ licenseUrl, releaseOnSuspend: true });
The option is off by default: resume then waits for a license round trip before encrypted media plays. Closing a temporary session stops its renewals; how soon the server frees the stream depends on its license duration.
FairPlay
FairPlay needs the application certificate from your key server. Pass its
URL as certificateUrl. eme-core fetches it before the first session.
emeFairplay({ certificateUrl: 'https://license.example.com/fairplay.cer' });
A player that builds its stages from a preset can pass the certificate to
eme-core instead, as certificateUrl or at runtime:
engine.drm.setCertificateUrl('https://license.example.com/fairplay.cer');
Key servers differ in the license request body they accept.
licenseBody |
Sends |
|---|---|
binary (default) |
The SPC bytes, as application/octet-stream |
form |
An spc= field with the SPC in base64, as a URL-encoded form |
FairPlay HLS in MPEG-TS uses SAMPLE-AES. Only Safari’s own player decrypts
it, so it plays through native HLS (see “DRM without the engine” below),
not through the engine. FairPlay in fMP4 (cbcs) plays through both.
ClearKey
ClearKey needs no server. Give the stage the keys and it answers license requests itself. It also works in headless browsers, so it is the one to test with.
emeCore({ clearKeys: { nrQFDeRLSAKTLifXUIPiZg: 'ABEiM0RVZneImaq7zN3u_w' } });
engine.drm and events
engine.drm.keySystem; // 'com.widevine.alpha' or null
engine.drm.sessions; // [{ keyId, status }]
| Event | When |
|---|---|
drm:encrypted |
Init data arrived from the media |
drm:keysystem |
A key system was selected |
drm:keystatus |
A key’s status changed, such as usable or expired |
drm:renewing |
An expired license is being renewed, with keyIds |
drm:released |
Suspend closed the key sessions, with sessions |
error |
With category drm when a step fails |
A license failure is fatal. Output restrictions and expired keys carry their own codes. Chapter 09 lists them.
DRM without the engine
On Safari you can play HLS natively by setting video.src. The engine does
not run then, but encrypted content still needs a license. attachEme
from mattebox/eme runs the same eme-core and key-system code on the
element, without an engine.
import { attachEme } from 'mattebox/eme';
import emeFairplay from 'mattebox/stages/eme-fairplay';
const drm = attachEme(video, {
keySystems: [emeFairplay({ certificateUrl: 'https://license.example.com/fairplay.cer' })],
licenseUrl: 'https://license.example.com/fairplay',
requestHook: (request) => {
request.headers.Authorization = `Bearer ${token}`;
},
});
video.src = 'https://cdn.example.com/stream.m3u8';
drm.on('drm:keysystem', () => console.log(drm.drm.keySystem));
drm.on('error', (error) => console.error(error.code));
// When the session ends:
drm.detach();
- Call
attachEmebefore you setsrc, so noencryptedevent is missed. - It takes the
eme-coreoptions from the table above, exceptreleaseOnSuspend. Calldetach()and attach again instead. keySystemslists the key-system stages to offer. ClearKey needs none.requestHookrewrites the URL and headers of license and certificate requests. The engine’s request hooks do not apply, since no engine runs.- Init data comes only from the element’s
encryptedevent. drm.drmis the same API asengine.drm.drm.ontakes the events in the table above.detach()closes every key session and clears the element’s MediaKeys.
The CDN bundles that carry DRM have it as mattebox.attachEme.
Example
A protected DASH and HLS player with a status pill.
import { mattebox } from 'mattebox';
import dashCmaf from 'mattebox/protocols/dash-cmaf';
import hlsCmaf from 'mattebox/protocols/hls-cmaf';
import emeCenc from 'mattebox/stages/eme-cenc';
import emeCore from 'mattebox/stages/eme-core';
import emeFairplay from 'mattebox/stages/eme-fairplay';
const engine = mattebox({
stages: [
hlsCmaf(),
dashCmaf(),
emeCore({
licenseUrls: {
'com.widevine.alpha': 'https://license.example.com/widevine',
'com.apple.fps': 'https://license.example.com/fairplay',
},
}),
emeCenc(),
emeFairplay({ certificateUrl: 'https://license.example.com/fairplay.cer' }),
],
});
engine.on('drm:keysystem', () => {
pill.textContent = engine.drm.keySystem.split('.').pop();
});
engine.on('error', (error) => {
if (error.category === 'drm') pill.textContent = error.code;
});
Next: 08 Legacy transport streams.