Skip to content

Commit

Permalink
Improve support for DRM key-systems and key handling
Browse files Browse the repository at this point in the history
  • Loading branch information
robwalch committed Oct 25, 2022
1 parent 63b9b79 commit 1149b62
Show file tree
Hide file tree
Showing 35 changed files with 2,911 additions and 1,556 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ HLS.js is written in [ECMAScript6] (`*.js`) and [TypeScript] (`*.ts`) (strongly
- AES-128 decryption
- SAMPLE-AES decryption (only supported if using MPEG-2 TS container)
- Encrypted media extensions (EME) support for DRM (digital rights management)
- 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==))
- FairPlay, PlayReady, Widevine CDMs with fmp4 segments
- CEA-608/708 captions
- WebVTT subtitles
- Alternate Audio Track Rendition (Master Playlist with Alternative Audio) for VoD and Live playlists
Expand Down Expand Up @@ -120,8 +120,7 @@ For a complete list of issues, see ["Top priorities" in the Release Planning and
- `#EXT-X-GAP` filling [#2940](https://github.com/video-dev/hls.js/issues/2940)
- `#EXT-X-I-FRAME-STREAM-INF` I-frame Media Playlist files
- `SAMPLE-AES` with fmp4, aac, mp3, vtt... segments (MPEG-2 TS only)
- FairPlay DRM with MPEG-2 TS content
- 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))
- FairPlay, PlayReady, Widevine DRM with MPEG-2 TS segments
- Advanced variant selection based on runtime media capabilities (See issues labeled [`media-capabilities`](https://github.com/video-dev/hls.js/labels/media-capabilities))
- 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))

Expand Down
84 changes: 70 additions & 14 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
- [`abrMaxWithRealBitrate`](#abrmaxwithrealbitrate)
- [`minAutoBitrate`](#minautobitrate)
- [`emeEnabled`](#emeEnabled)
- [`useEmeEncryptedEvent`](#useEmeEncryptedEvent)
- [`widevineLicenseUrl`](#widevineLicenseUrl)
- [`licenseXhrSetup`](#licenseXhrSetup)
- [`licenseResponseCallback`](#licenseResponseCallback)
Expand Down Expand Up @@ -399,6 +400,7 @@ var config = {
maxLoadingDelay: 4,
minAutoBitrate: 0,
emeEnabled: false,
useEmeEncryptedEvent: false,
widevineLicenseUrl: undefined,
licenseXhrSetup: undefined,
drmSystems: {},
Expand Down Expand Up @@ -1192,6 +1194,12 @@ Useful when browser or tab of the browser is not in the focus and bandwidth drop

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

### `useEmeEncryptedEvent`

(default: `false`)

Set to `true` to use media "encrypted" event initData and ignore manifest DRM keys.

### `widevineLicenseUrl`

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

### `licenseXhrSetup`

(default: `undefined`, type `(xhr: XMLHttpRequest, url: string) => void`)
(default: `undefined`, type `(xhr: XMLHttpRequest, url: string, keyContext: MediaKeySessionContext, licenseChallenge: Uint8Array) => void`)

A pre-processor function for modifying the `XMLHttpRequest` and request url (using `xhr.open`) prior to sending the license request.
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.

```js
var config = {
licenseXhrSetup: function (xhr, url) {
xhr.withCredentials = true; // do send cookies
if (!xhr.readyState) {
// Call open to change the method (default is POST) or modify the url
xhr.open('GET', url, true);
// Append headers after opening
licenseXhrSetup: function (xhr, url, keyContext, licenseChallenge) {
let payload = licenseChallenge;

// Send cookies with request
xhr.withCredentials = true;

// Call open to change the method (default is POST), modify the url, or set request headers
xhr.open('POST', url, true);

// call xhr.setRequestHeader after xhr.open otherwise licenseXhrSetup will throw and be called a second time after HLS.js call xhr.open
if (keyContext.keySystem === 'com.apple.fps') {
xhr.setRequestHeader('Content-Type', 'application/json');
payload = JSON.stringify({
keyData: base64Encode(keyContext.decryptdata?.keyId),
licenseChallenge: base64Encode(licenseChallenge),
});
} else {
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
}

// Return the desired payload or a Promise<Uint8Array|void>
// return Promise.resolve(payload);
return payload;
},
};
```
### `licenseResponseCallback`
(default: `undefined`, type `(xhr: XMLHttpRequest, url: string) => data: ArrayBuffer`)
(default: `undefined`, type `(xhr: XMLHttpRequest, url: string, keyContext: MediaKeySessionContext) => data: ArrayBuffer`)
A post-processor function for modifying the license response before passing it to the key-session (`MediaKeySession.update`).
```js
var config = {
licenseResponseCallback: function (xhr, url, keyContext) {
const keySystem = keyContext.keySystem;
const response = xhr.response;
if (keyContext.keySystem === 'com.apple.fps') {
try {
const responseObject = JSON.parse(
new TextDecoder().decode(response).trim();
);
const keyResponse = responseObject['fairplay-streaming-response']['streaming-keys'][0];
return base64Decode(keyResponse.ckc);
} catch (error) {
console.error(error);
}
}
return response;
}
```
### `drmSystems`
(default: `{}`)
Set `licenseUrl` and `serverCertificateUrl` for a given keySystem to your own DRM provider. `serverCertificateUrl` is not mandatory. Ex:
Set `licenseUrl` and `serverCertificateUrl` for a given key-system to your own DRM provider. `serverCertificateUrl` is not mandatory. Ex:
```js
{
drmSystems: {
'com.widevine.alpha': {
licenseUrl: 'https://proxy.uat.widevine.com/proxy',
serverCertificateUrl: 'https://storage.googleapis.com/wvmedia/cert/cert_license_widevine_com_uat.bin'
licenseUrl: 'https://your-widevine-license-server/path',
serverCertificateUrl: 'https://optional-server-certificate/path/cert.bin'
}
}
```
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).
### `drmSystemOptions`
(default: `{}`)
Expand All @@ -1258,7 +1303,18 @@ With the default argument, `''` will be specified for each option (_i.e. no spec
(default: A function that returns the result of `window.navigator.requestMediaKeySystemAccess.bind(window.navigator)` or `null`)
Allows for the customization of `window.navigator.requestMediaKeySystemAccess`.
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:
```js
var hls new Hls({
requestMediaKeySystemAccessFunc: (keySystem, supportedConfigurations) => {
if (keySystem === 'com.microsoft.playready') {
keySystem = 'com.microsoft.playready.recommendation';
}
return navigator.requestMediaKeySystemAccess(keySystem, supportedConfigurations);
}
});
```
### `cmcd`
Expand Down
20 changes: 16 additions & 4 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import BufferController from './controller/buffer-controller';
import { TimelineController } from './controller/timeline-controller';
import CapLevelController from './controller/cap-level-controller';
import FPSController from './controller/fps-controller';
import EMEController from './controller/eme-controller';
import EMEController, {
MediaKeySessionContext,
} from './controller/eme-controller';
import CMCDController from './controller/cmcd-controller';
import XhrLoader from './utils/xhr-loader';
import FetchLoader, { fetchSupported } from './utils/fetch-loader';
import Cues from './utils/cues';
import { requestMediaKeySystemAccess } from './utils/mediakeys-helper';
import { ILogger, logger } from './utils/logger';

import type Hls from './hls';
import type { CuesInterface } from './utils/cues';
import type { MediaKeyFunc, KeySystems } from './utils/mediakeys-helper';
import type {
Expand Down Expand Up @@ -57,6 +60,10 @@ export type CMCDControllerConfig = {
export type DRMSystemOptions = {
audioRobustness?: string;
videoRobustness?: string;
persistentState?: MediaKeysRequirement;
distinctiveIdentifier?: MediaKeysRequirement;
sessionTypes?: string[];
sessionType?: string;
};

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

export type EMEControllerConfig = {
licenseXhrSetup?: (
this: Hls,
xhr: XMLHttpRequest,
url: string,
keySystem: KeySystems
) => void | Promise<void>;
keyContext: MediaKeySessionContext,
licenseChallenge: Uint8Array
) => void | Promise<Uint8Array | void>;
licenseResponseCallback?: (
this: Hls,
xhr: XMLHttpRequest,
url: string,
keySystem: KeySystems
keyContext: MediaKeySessionContext
) => ArrayBuffer;
emeEnabled: boolean;
useEmeEncryptedEvent: boolean;
widevineLicenseUrl?: string;
drmSystems: DRMSystemsConfiguration;
drmSystemOptions: DRMSystemOptions;
Expand Down Expand Up @@ -300,6 +311,7 @@ export const hlsDefaultConfig: HlsConfig = {
maxLoadingDelay: 4, // used by abr-controller
minAutoBitrate: 0, // used by hls
emeEnabled: false, // used by eme-controller
useEmeEncryptedEvent: false, // used by eme-controller
widevineLicenseUrl: undefined, // used by eme-controller
drmSystems: {}, // used by eme-controller
drmSystemOptions: {}, // used by eme-controller
Expand Down
22 changes: 15 additions & 7 deletions src/controller/abr-controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import EwmaBandWidthEstimator from '../utils/ewma-bandwidth-estimator';
import { Events } from '../events';
import { BufferHelper } from '../utils/buffer-helper';
import { ErrorDetails } from '../errors';
import { ErrorDetails, ErrorTypes } from '../errors';
import { PlaylistLevelType } from '../types/loader';
import { logger } from '../utils/logger';
import type { Bufferable } from '../utils/buffer-helper';
Expand Down Expand Up @@ -277,13 +277,21 @@ class AbrController implements ComponentAPI {

protected onError(event: Events.ERROR, data: ErrorData) {
// stop timer in case of frag loading error
switch (data.details) {
case ErrorDetails.FRAG_LOAD_ERROR:
case ErrorDetails.FRAG_LOAD_TIMEOUT:
if (data.frag?.type === PlaylistLevelType.MAIN) {
if (data.type === ErrorTypes.KEY_SYSTEM_ERROR) {
this.clearTimer();
break;
default:
break;
return;
}
switch (data.details) {
case ErrorDetails.FRAG_LOAD_ERROR:
case ErrorDetails.FRAG_LOAD_TIMEOUT:
case ErrorDetails.KEY_LOAD_ERROR:
case ErrorDetails.KEY_LOAD_TIMEOUT:
this.clearTimer();
break;
default:
break;
}
}
}

Expand Down
15 changes: 11 additions & 4 deletions src/controller/audio-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import TransmuxerInterface from '../demux/transmuxer-interface';
import { ChunkMetadata } from '../types/transmuxer';
import { fragmentWithinToleranceTest } from './fragment-finders';
import { alignMediaPlaylistByPDT } from '../utils/discontinuities';
import { ErrorDetails } from '../errors';
import { ErrorDetails, ErrorTypes } from '../errors';
import type { NetworkComponentAPI } from '../types/component-api';
import type Hls from '../hls';
import type { FragmentTracker } from './fragment-tracker';
import type KeyLoader from '../loader/key-loader';
import type { TransmuxerResult } from '../types/transmuxer';
import type Hls from '../hls';
import type { LevelDetails } from '../loader/level-details';
import type { TrackSet } from '../types/track';
import type {
Expand Down Expand Up @@ -56,8 +57,12 @@ class AudioStreamController
private bufferFlushed: boolean = false;
private cachedTrackLoadedData: TrackLoadedData | null = null;

constructor(hls: Hls, fragmentTracker: FragmentTracker) {
super(hls, fragmentTracker, '[audio-stream-controller]');
constructor(
hls: Hls,
fragmentTracker: FragmentTracker,
keyLoader: KeyLoader
) {
super(hls, fragmentTracker, keyLoader, '[audio-stream-controller]');
this._registerListeners();
}

Expand Down Expand Up @@ -625,6 +630,8 @@ class AudioStreamController
case ErrorDetails.FRAG_LOAD_TIMEOUT:
case ErrorDetails.KEY_LOAD_ERROR:
case ErrorDetails.KEY_LOAD_TIMEOUT:
case ErrorDetails.KEY_SYSTEM_NO_SESSION:
case ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED:
// TODO: Skip fragments that do not belong to this.fragCurrent audio-group id
this.onFragmentOrKeyLoadError(PlaylistLevelType.AUDIO, data);
break;
Expand Down
36 changes: 27 additions & 9 deletions src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FragmentState } from './fragment-tracker';
import { Bufferable, BufferHelper, BufferInfo } from '../utils/buffer-helper';
import { logger } from '../utils/logger';
import { Events } from '../events';
import { ErrorDetails } from '../errors';
import { ErrorDetails, ErrorTypes } from '../errors';
import { ChunkMetadata } from '../types/transmuxer';
import { appendUint8Array } from '../utils/mp4-tools';
import { alignStream } from '../utils/discontinuities';
Expand Down Expand Up @@ -99,14 +99,19 @@ export default class BaseStreamController
protected log: (msg: any) => void;
protected warn: (msg: any) => void;

constructor(hls: Hls, fragmentTracker: FragmentTracker, logPrefix: string) {
constructor(
hls: Hls,
fragmentTracker: FragmentTracker,
keyLoader: KeyLoader,
logPrefix: string
) {
super();
this.logPrefix = logPrefix;
this.log = logger.log.bind(logger, `${logPrefix}:`);
this.warn = logger.warn.bind(logger, `${logPrefix}:`);
this.hls = hls;
this.fragmentLoader = new FragmentLoader(hls.config);
this.keyLoader = new KeyLoader(hls.config);
this.keyLoader = keyLoader;
this.fragmentTracker = fragmentTracker;
this.config = hls.config;
this.decrypter = new Decrypter(hls.config);
Expand Down Expand Up @@ -205,6 +210,9 @@ export default class BaseStreamController
media.removeEventListener('ended', this.onvended);
this.onvseeking = this.onvended = null;
}
if (this.keyLoader) {
this.keyLoader.detach();
}
this.media = this.mediaBuffer = null;
this.loadedmetadata = false;
this.fragmentTracker.removeAllFragments();
Expand Down Expand Up @@ -558,7 +566,7 @@ export default class BaseStreamController
this.state = State.KEY_LOADING;
this.fragCurrent = frag;
keyLoadingPromise = this.keyLoader.load(frag).then((keyLoadedData) => {
if (keyLoadedData && !this.fragContextChanged(keyLoadedData.frag)) {
if (!this.fragContextChanged(keyLoadedData.frag)) {
this.hls.trigger(Events.KEY_LOADED, keyLoadedData);
return keyLoadedData;
}
Expand Down Expand Up @@ -601,7 +609,7 @@ export default class BaseStreamController
.then((keyLoadedData) => {
if (
!keyLoadedData ||
this.fragContextChanged(keyLoadedData?.frag)
this.fragContextChanged(keyLoadedData.frag)
) {
return null;
}
Expand Down Expand Up @@ -719,11 +727,21 @@ export default class BaseStreamController
);
}

private handleFragLoadError({ data }: LoadError) {
if (data && data.details === ErrorDetails.INTERNAL_ABORTED) {
this.handleFragLoadAborted(data.frag, data.part);
private handleFragLoadError(error: LoadError | Error) {
if ('data' in error) {
const data = error.data;
if (error.data && data.details === ErrorDetails.INTERNAL_ABORTED) {
this.handleFragLoadAborted(data.frag, data.part);
} else {
this.hls.trigger(Events.ERROR, data as ErrorData);
}
} else {
this.hls.trigger(Events.ERROR, data as ErrorData);
this.hls.trigger(Events.ERROR, {
type: ErrorTypes.OTHER_ERROR,
details: ErrorDetails.INTERNAL_EXCEPTION,
err: error,
fatal: true,
});
}
return null;
}
Expand Down
Loading

0 comments on commit 1149b62

Please sign in to comment.