Skip to content

Commit 554e93e

Browse files
committed
Improve support for DRM key-systems and key handling
Resolves #2833 #2737 #4318 #4538
1 parent b5427b5 commit 554e93e

35 files changed

+2911
-1556
lines changed

README.md

+2-3
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ HLS.js is written in [ECMAScript6] (`*.js`) and [TypeScript] (`*.ts`) (strongly
4545
- AES-128 decryption
4646
- SAMPLE-AES decryption (only supported if using MPEG-2 TS container)
4747
- Encrypted media extensions (EME) support for DRM (digital rights management)
48-
- Widevine CDM (only tested with [shaka-packager](https://github.com/google/shaka-packager) test-stream on [the demo page](https://hls-js.netlify.app/demo/?src=https%3A%2F%2Fstorage.googleapis.com%2Fshaka-demo-assets%2Fangel-one-widevine-hls%2Fhls.m3u8&demoConfig=eyJlbmFibGVTdHJlYW1pbmciOnRydWUsImF1dG9SZWNvdmVyRXJyb3IiOnRydWUsInN0b3BPblN0YWxsIjpmYWxzZSwiZHVtcGZNUDQiOmZhbHNlLCJsZXZlbENhcHBpbmciOi0xLCJsaW1pdE1ldHJpY3MiOi0xfQ==))
48+
- FairPlay, PlayReady, Widevine CDMs with fmp4 segments
4949
- CEA-608/708 captions
5050
- WebVTT subtitles
5151
- Alternate Audio Track Rendition (Master Playlist with Alternative Audio) for VoD and Live playlists
@@ -120,8 +120,7 @@ For a complete list of issues, see ["Top priorities" in the Release Planning and
120120
- `#EXT-X-GAP` filling [#2940](https://github.com/video-dev/hls.js/issues/2940)
121121
- `#EXT-X-I-FRAME-STREAM-INF` I-frame Media Playlist files
122122
- `SAMPLE-AES` with fmp4, aac, mp3, vtt... segments (MPEG-2 TS only)
123-
- FairPlay DRM with MPEG-2 TS content
124-
- PlayReady (See [#3779](https://github.com/video-dev/hls.js/issues/3779) and [issues labeled DRM](https://github.com/video-dev/hls.js/issues?q=is%3Aissue+is%3Aopen+label%3ADRM))
123+
- FairPlay, PlayReady, Widevine DRM with MPEG-2 TS segments
125124
- Advanced variant selection based on runtime media capabilities (See issues labeled [`media-capabilities`](https://github.com/video-dev/hls.js/labels/media-capabilities))
126125
- MP3 elementary stream audio in IE and Edge (<=18) on Windows 10 (See [#1641](https://github.com/video-dev/hls.js/issues/1641) and [Microsoft answers forum](https://answers.microsoft.com/en-us/ie/forum/all/ie11-on-windows-10-cannot-play-hls-with-mp3/2da994b5-8dec-4ae9-9201-7d138ede49d9))
127126

docs/API.md

+70-14
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
- [`abrMaxWithRealBitrate`](#abrmaxwithrealbitrate)
9595
- [`minAutoBitrate`](#minautobitrate)
9696
- [`emeEnabled`](#emeEnabled)
97+
- [`useEmeEncryptedEvent`](#useEmeEncryptedEvent)
9798
- [`widevineLicenseUrl`](#widevineLicenseUrl)
9899
- [`licenseXhrSetup`](#licenseXhrSetup)
99100
- [`licenseResponseCallback`](#licenseResponseCallback)
@@ -399,6 +400,7 @@ var config = {
399400
maxLoadingDelay: 4,
400401
minAutoBitrate: 0,
401402
emeEnabled: false,
403+
useEmeEncryptedEvent: false,
402404
widevineLicenseUrl: undefined,
403405
licenseXhrSetup: undefined,
404406
drmSystems: {},
@@ -1192,6 +1194,12 @@ Useful when browser or tab of the browser is not in the focus and bandwidth drop
11921194

11931195
Set to `true` to enable DRM key system access and license retrieval.
11941196

1197+
### `useEmeEncryptedEvent`
1198+
1199+
(default: `false`)
1200+
1201+
Set to `true` to use media "encrypted" event initData and ignore manifest DRM keys.
1202+
11951203
### `widevineLicenseUrl`
11961204

11971205
(default: `undefined`)
@@ -1200,45 +1208,82 @@ The Widevine license server URL.
12001208

12011209
### `licenseXhrSetup`
12021210

1203-
(default: `undefined`, type `(xhr: XMLHttpRequest, url: string) => void`)
1211+
(default: `undefined`, type `(xhr: XMLHttpRequest, url: string, keyContext: MediaKeySessionContext, licenseChallenge: Uint8Array) => void`)
12041212

1205-
A pre-processor function for modifying the `XMLHttpRequest` and request url (using `xhr.open`) prior to sending the license request.
1213+
A pre-processor function for modifying license requests. The license request URL, request headers, and payload can all be modified prior to sending the license request, based on operating conditions, the current key-session, and key-system.
12061214

12071215
```js
12081216
var config = {
1209-
licenseXhrSetup: function (xhr, url) {
1210-
xhr.withCredentials = true; // do send cookies
1211-
if (!xhr.readyState) {
1212-
// Call open to change the method (default is POST) or modify the url
1213-
xhr.open('GET', url, true);
1214-
// Append headers after opening
1217+
licenseXhrSetup: function (xhr, url, keyContext, licenseChallenge) {
1218+
let payload = licenseChallenge;
1219+
1220+
// Send cookies with request
1221+
xhr.withCredentials = true;
1222+
1223+
// Call open to change the method (default is POST), modify the url, or set request headers
1224+
xhr.open('POST', url, true);
1225+
1226+
// call xhr.setRequestHeader after xhr.open otherwise licenseXhrSetup will throw and be called a second time after HLS.js call xhr.open
1227+
if (keyContext.keySystem === 'com.apple.fps') {
1228+
xhr.setRequestHeader('Content-Type', 'application/json');
1229+
payload = JSON.stringify({
1230+
keyData: base64Encode(keyContext.decryptdata?.keyId),
1231+
licenseChallenge: base64Encode(licenseChallenge),
1232+
});
1233+
} else {
12151234
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
12161235
}
1236+
1237+
// Return the desired payload or a Promise<Uint8Array|void>
1238+
// return Promise.resolve(payload);
1239+
return payload;
12171240
},
12181241
};
12191242
```
12201243
12211244
### `licenseResponseCallback`
12221245
1223-
(default: `undefined`, type `(xhr: XMLHttpRequest, url: string) => data: ArrayBuffer`)
1246+
(default: `undefined`, type `(xhr: XMLHttpRequest, url: string, keyContext: MediaKeySessionContext) => data: ArrayBuffer`)
12241247
12251248
A post-processor function for modifying the license response before passing it to the key-session (`MediaKeySession.update`).
12261249
1250+
```js
1251+
var config = {
1252+
licenseResponseCallback: function (xhr, url, keyContext) {
1253+
const keySystem = keyContext.keySystem;
1254+
const response = xhr.response;
1255+
if (keyContext.keySystem === 'com.apple.fps') {
1256+
try {
1257+
const responseObject = JSON.parse(
1258+
new TextDecoder().decode(response).trim();
1259+
);
1260+
const keyResponse = responseObject['fairplay-streaming-response']['streaming-keys'][0];
1261+
return base64Decode(keyResponse.ckc);
1262+
} catch (error) {
1263+
console.error(error);
1264+
}
1265+
}
1266+
return response;
1267+
}
1268+
```
1269+
12271270
### `drmSystems`
12281271
12291272
(default: `{}`)
12301273
1231-
Set `licenseUrl` and `serverCertificateUrl` for a given keySystem to your own DRM provider. `serverCertificateUrl` is not mandatory. Ex:
1274+
Set `licenseUrl` and `serverCertificateUrl` for a given key-system to your own DRM provider. `serverCertificateUrl` is not mandatory. Ex:
12321275
12331276
```js
1234-
{
1277+
drmSystems: {
12351278
'com.widevine.alpha': {
1236-
licenseUrl: 'https://proxy.uat.widevine.com/proxy',
1237-
serverCertificateUrl: 'https://storage.googleapis.com/wvmedia/cert/cert_license_widevine_com_uat.bin'
1279+
licenseUrl: 'https://your-widevine-license-server/path',
1280+
serverCertificateUrl: 'https://optional-server-certificate/path/cert.bin'
12381281
}
12391282
}
12401283
```
12411284
1285+
Supported key-systems include 'com.apple.fps', 'com.microsoft.playready', 'com.widevine.alpha', and 'org.w3.clearkey'. Mapping to other values in key-system access requests can be done by customizing [`requestMediaKeySystemAccessFunc`](#requestMediaKeySystemAccessFunc).
1286+
12421287
### `drmSystemOptions`
12431288
12441289
(default: `{}`)
@@ -1258,7 +1303,18 @@ With the default argument, `''` will be specified for each option (_i.e. no spec
12581303
12591304
(default: A function that returns the result of `window.navigator.requestMediaKeySystemAccess.bind(window.navigator)` or `null`)
12601305
1261-
Allows for the customization of `window.navigator.requestMediaKeySystemAccess`.
1306+
Allows for the customization of `window.navigator.requestMediaKeySystemAccess`. This can be used to map key-system access request to from a supported value to a custom one:
1307+
1308+
```js
1309+
var hls new Hls({
1310+
requestMediaKeySystemAccessFunc: (keySystem, supportedConfigurations) => {
1311+
if (keySystem === 'com.microsoft.playready') {
1312+
keySystem = 'com.microsoft.playready.recommendation';
1313+
}
1314+
return navigator.requestMediaKeySystemAccess(keySystem, supportedConfigurations);
1315+
}
1316+
});
1317+
```
12621318
12631319
### `cmcd`
12641320

src/config.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ import BufferController from './controller/buffer-controller';
77
import { TimelineController } from './controller/timeline-controller';
88
import CapLevelController from './controller/cap-level-controller';
99
import FPSController from './controller/fps-controller';
10-
import EMEController from './controller/eme-controller';
10+
import EMEController, {
11+
MediaKeySessionContext,
12+
} from './controller/eme-controller';
1113
import CMCDController from './controller/cmcd-controller';
1214
import XhrLoader from './utils/xhr-loader';
1315
import FetchLoader, { fetchSupported } from './utils/fetch-loader';
1416
import Cues from './utils/cues';
1517
import { requestMediaKeySystemAccess } from './utils/mediakeys-helper';
1618
import { ILogger, logger } from './utils/logger';
1719

20+
import type Hls from './hls';
1821
import type { CuesInterface } from './utils/cues';
1922
import type { MediaKeyFunc, KeySystems } from './utils/mediakeys-helper';
2023
import type {
@@ -57,6 +60,10 @@ export type CMCDControllerConfig = {
5760
export type DRMSystemOptions = {
5861
audioRobustness?: string;
5962
videoRobustness?: string;
63+
persistentState?: MediaKeysRequirement;
64+
distinctiveIdentifier?: MediaKeysRequirement;
65+
sessionTypes?: string[];
66+
sessionType?: string;
6067
};
6168

6269
export type DRMSystemConfiguration = {
@@ -70,16 +77,20 @@ export type DRMSystemsConfiguration = Partial<
7077

7178
export type EMEControllerConfig = {
7279
licenseXhrSetup?: (
80+
this: Hls,
7381
xhr: XMLHttpRequest,
7482
url: string,
75-
keySystem: KeySystems
76-
) => void | Promise<void>;
83+
keyContext: MediaKeySessionContext,
84+
licenseChallenge: Uint8Array
85+
) => void | Promise<Uint8Array | void>;
7786
licenseResponseCallback?: (
87+
this: Hls,
7888
xhr: XMLHttpRequest,
7989
url: string,
80-
keySystem: KeySystems
90+
keyContext: MediaKeySessionContext
8191
) => ArrayBuffer;
8292
emeEnabled: boolean;
93+
useEmeEncryptedEvent: boolean;
8394
widevineLicenseUrl?: string;
8495
drmSystems: DRMSystemsConfiguration;
8596
drmSystemOptions: DRMSystemOptions;
@@ -300,6 +311,7 @@ export const hlsDefaultConfig: HlsConfig = {
300311
maxLoadingDelay: 4, // used by abr-controller
301312
minAutoBitrate: 0, // used by hls
302313
emeEnabled: false, // used by eme-controller
314+
useEmeEncryptedEvent: false, // used by eme-controller
303315
widevineLicenseUrl: undefined, // used by eme-controller
304316
drmSystems: {}, // used by eme-controller
305317
drmSystemOptions: {}, // used by eme-controller

src/controller/abr-controller.ts

+15-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import EwmaBandWidthEstimator from '../utils/ewma-bandwidth-estimator';
22
import { Events } from '../events';
33
import { BufferHelper } from '../utils/buffer-helper';
4-
import { ErrorDetails } from '../errors';
4+
import { ErrorDetails, ErrorTypes } from '../errors';
55
import { PlaylistLevelType } from '../types/loader';
66
import { logger } from '../utils/logger';
77
import type { Bufferable } from '../utils/buffer-helper';
@@ -277,13 +277,21 @@ class AbrController implements ComponentAPI {
277277

278278
protected onError(event: Events.ERROR, data: ErrorData) {
279279
// stop timer in case of frag loading error
280-
switch (data.details) {
281-
case ErrorDetails.FRAG_LOAD_ERROR:
282-
case ErrorDetails.FRAG_LOAD_TIMEOUT:
280+
if (data.frag?.type === PlaylistLevelType.MAIN) {
281+
if (data.type === ErrorTypes.KEY_SYSTEM_ERROR) {
283282
this.clearTimer();
284-
break;
285-
default:
286-
break;
283+
return;
284+
}
285+
switch (data.details) {
286+
case ErrorDetails.FRAG_LOAD_ERROR:
287+
case ErrorDetails.FRAG_LOAD_TIMEOUT:
288+
case ErrorDetails.KEY_LOAD_ERROR:
289+
case ErrorDetails.KEY_LOAD_TIMEOUT:
290+
this.clearTimer();
291+
break;
292+
default:
293+
break;
294+
}
287295
}
288296
}
289297

src/controller/audio-stream-controller.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ import TransmuxerInterface from '../demux/transmuxer-interface';
1010
import { ChunkMetadata } from '../types/transmuxer';
1111
import { fragmentWithinToleranceTest } from './fragment-finders';
1212
import { alignMediaPlaylistByPDT } from '../utils/discontinuities';
13-
import { ErrorDetails } from '../errors';
13+
import { ErrorDetails, ErrorTypes } from '../errors';
1414
import type { NetworkComponentAPI } from '../types/component-api';
15+
import type Hls from '../hls';
1516
import type { FragmentTracker } from './fragment-tracker';
17+
import type KeyLoader from '../loader/key-loader';
1618
import type { TransmuxerResult } from '../types/transmuxer';
17-
import type Hls from '../hls';
1819
import type { LevelDetails } from '../loader/level-details';
1920
import type { TrackSet } from '../types/track';
2021
import type {
@@ -56,8 +57,12 @@ class AudioStreamController
5657
private bufferFlushed: boolean = false;
5758
private cachedTrackLoadedData: TrackLoadedData | null = null;
5859

59-
constructor(hls: Hls, fragmentTracker: FragmentTracker) {
60-
super(hls, fragmentTracker, '[audio-stream-controller]');
60+
constructor(
61+
hls: Hls,
62+
fragmentTracker: FragmentTracker,
63+
keyLoader: KeyLoader
64+
) {
65+
super(hls, fragmentTracker, keyLoader, '[audio-stream-controller]');
6166
this._registerListeners();
6267
}
6368

@@ -624,6 +629,8 @@ class AudioStreamController
624629
case ErrorDetails.FRAG_LOAD_TIMEOUT:
625630
case ErrorDetails.KEY_LOAD_ERROR:
626631
case ErrorDetails.KEY_LOAD_TIMEOUT:
632+
case ErrorDetails.KEY_SYSTEM_NO_SESSION:
633+
case ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED:
627634
// TODO: Skip fragments that do not belong to this.fragCurrent audio-group id
628635
this.onFragmentOrKeyLoadError(PlaylistLevelType.AUDIO, data);
629636
break;

src/controller/base-stream-controller.ts

+27-9
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { FragmentState } from './fragment-tracker';
33
import { Bufferable, BufferHelper, BufferInfo } from '../utils/buffer-helper';
44
import { logger } from '../utils/logger';
55
import { Events } from '../events';
6-
import { ErrorDetails } from '../errors';
6+
import { ErrorDetails, ErrorTypes } from '../errors';
77
import { ChunkMetadata } from '../types/transmuxer';
88
import { appendUint8Array } from '../utils/mp4-tools';
99
import { alignStream } from '../utils/discontinuities';
@@ -99,14 +99,19 @@ export default class BaseStreamController
9999
protected log: (msg: any) => void;
100100
protected warn: (msg: any) => void;
101101

102-
constructor(hls: Hls, fragmentTracker: FragmentTracker, logPrefix: string) {
102+
constructor(
103+
hls: Hls,
104+
fragmentTracker: FragmentTracker,
105+
keyLoader: KeyLoader,
106+
logPrefix: string
107+
) {
103108
super();
104109
this.logPrefix = logPrefix;
105110
this.log = logger.log.bind(logger, `${logPrefix}:`);
106111
this.warn = logger.warn.bind(logger, `${logPrefix}:`);
107112
this.hls = hls;
108113
this.fragmentLoader = new FragmentLoader(hls.config);
109-
this.keyLoader = new KeyLoader(hls.config);
114+
this.keyLoader = keyLoader;
110115
this.fragmentTracker = fragmentTracker;
111116
this.config = hls.config;
112117
this.decrypter = new Decrypter(hls.config);
@@ -205,6 +210,9 @@ export default class BaseStreamController
205210
media.removeEventListener('ended', this.onvended);
206211
this.onvseeking = this.onvended = null;
207212
}
213+
if (this.keyLoader) {
214+
this.keyLoader.detach();
215+
}
208216
this.media = this.mediaBuffer = null;
209217
this.loadedmetadata = false;
210218
this.fragmentTracker.removeAllFragments();
@@ -566,7 +574,7 @@ export default class BaseStreamController
566574
this.state = State.KEY_LOADING;
567575
this.fragCurrent = frag;
568576
keyLoadingPromise = this.keyLoader.load(frag).then((keyLoadedData) => {
569-
if (keyLoadedData && !this.fragContextChanged(keyLoadedData.frag)) {
577+
if (!this.fragContextChanged(keyLoadedData.frag)) {
570578
this.hls.trigger(Events.KEY_LOADED, keyLoadedData);
571579
return keyLoadedData;
572580
}
@@ -609,7 +617,7 @@ export default class BaseStreamController
609617
.then((keyLoadedData) => {
610618
if (
611619
!keyLoadedData ||
612-
this.fragContextChanged(keyLoadedData?.frag)
620+
this.fragContextChanged(keyLoadedData.frag)
613621
) {
614622
return null;
615623
}
@@ -727,11 +735,21 @@ export default class BaseStreamController
727735
);
728736
}
729737

730-
private handleFragLoadError({ data }: LoadError) {
731-
if (data && data.details === ErrorDetails.INTERNAL_ABORTED) {
732-
this.handleFragLoadAborted(data.frag, data.part);
738+
private handleFragLoadError(error: LoadError | Error) {
739+
if ('data' in error) {
740+
const data = error.data;
741+
if (error.data && data.details === ErrorDetails.INTERNAL_ABORTED) {
742+
this.handleFragLoadAborted(data.frag, data.part);
743+
} else {
744+
this.hls.trigger(Events.ERROR, data as ErrorData);
745+
}
733746
} else {
734-
this.hls.trigger(Events.ERROR, data as ErrorData);
747+
this.hls.trigger(Events.ERROR, {
748+
type: ErrorTypes.OTHER_ERROR,
749+
details: ErrorDetails.INTERNAL_EXCEPTION,
750+
err: error,
751+
fatal: true,
752+
});
735753
}
736754
return null;
737755
}

0 commit comments

Comments
 (0)