Skip to content
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4fb5fdb
add support for pssh in manifest
ssreed Sep 13, 2018
b1e50f8
move util function into it's own file
ssreed Sep 13, 2018
3b423f5
add docs, clean up names
ssreed Sep 13, 2018
5d2a4ac
add unit tests
ssreed Sep 13, 2018
fef1409
add support for multi-drm manifests
ssreed Sep 15, 2018
6713568
add docs
ssreed Sep 15, 2018
294d8af
add playready support
ssreed Sep 15, 2018
8236809
clean up
ssreed Sep 15, 2018
d77f6d0
move playready header logic to separate function
ssreed Sep 17, 2018
d04049e
update playready challenge logic
ssreed Sep 17, 2018
685e6df
fix: use for loop instead of forEach
ssreed Sep 17, 2018
7c670cc
move playready header logic to separate file
ssreed Sep 19, 2018
0401518
fix typo
ssreed Sep 19, 2018
00025d1
don't initialize emeController if eme isn't enabled
ssreed Sep 20, 2018
09d9660
simplify playready headers
ssreed Oct 26, 2018
a9d9e8f
clarify and update function name to more accurately reflect returned …
ssreed Nov 9, 2018
1c9021f
Merge branch 'master' of https://github.com/video-dev/hls.js into add…
ssreed Nov 9, 2018
56a357f
optimize and improve readability of capturing initData
ssreed Nov 10, 2018
ca9bcc3
fix unit test
ssreed Nov 10, 2018
49e22ca
remove unnecessary code
ssreed Nov 10, 2018
897257d
add documentation, add initDataTypes enum
ssreed Nov 13, 2018
177b1e7
fix functional test
ssreed Nov 13, 2018
61494eb
add key rotation support
Jan 23, 2019
32215cc
Merge branch 'master' into add-playready
ssreed Jan 28, 2019
e840bfb
fix unit test
ssreed Jan 28, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@
- [`abrBandWidthUpFactor`](#abrbandwidthupfactor)
- [`abrMaxWithRealBitrate`](#abrmaxwithrealbitrate)
- [`minAutoBitrate`](#minautobitrate)
- [`emeEnabled`](#emeenabled)
- [`licenseXhrSetup`](#licensexhrsetup)
- [`drmSystem`](#drmsystem)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i m not sure we need that, or we need something different in terms of API.

at least you need to expose an enum on the API with the possible values, and/or document what values

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There could be a better way to do this but I was leaving it up to the consumer to figure out what type of DRM system is supported for a particular environment. So for example, if there was a manifest that is multi DRM, the client can choose their preferred system based on the browser/CDM.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but as a common purpose project, our responsability is finding common denominator solutions with future proof APIs that will cover many possible use-cases.

For example what we should do is allow to configure a preference priority-list of DRM systems. Then, the key-systems are tried for existence on the platform in that order.

@tchakabam tchakabam Sep 28, 2018

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw such a config parameter is optional, isn'it? :) We can figure out inside our code if there is a DRM system that fits the ones in the media manifest.

So, what we really want in the end is to set a preferred system (or a prio list of), and the values in there would be the values of the key-system enum, right?

- [`widevineLicenseUrl`](#widevinelicenseurl)
- [`playreadyLicenseUrl`](#playreadylicenseurl)
- [Video Binding/Unbinding API](#video-bindingunbinding-api)
- [`hls.attachMedia(videoElement)`](#hlsattachmediavideoelement)
- [`hls.detachMedia()`](#hlsdetachmedia)
Expand Down Expand Up @@ -964,6 +969,47 @@ then if config value is set to `true`, ABR will use 2.5 Mb/s for this quality le
Return the capping/min bandwidth value that could be used by automatic level selection algorithm.
Useful when browser or tab of the browser is not in the focus and bandwidth drops

### `emeEnabled`

(default: `false`)

Whether or not to enable eme.

### `licenseXhrSetup`

(default: `undefined`)

`XMLHttpRequest` customization callback for default XHR based loader.

Parameter should be a function with two arguments `(xhr: XMLHttpRequest, url: string)`.
If `licenseXhrSetup` is specified, default loader will invoke it before calling `xhr.send()`.
This allows user to easily modify/setup XHR. See example below.

```js
var config = {
licenseXhrSetup: function(xhr, url) {
xhr.setRequestHeader('Content-Type', 'text/xml; charset=utf-8'); //indicate resource is xml
}
}
```

### `drmSystem`

(default: `undefined`)

Which DRM system use. (`WIDEVINE` or `PLAYREADY`)

### `widevineLicenseUrl`

(default: `undefined`)

Specify widevine license url.

### `playreadyLicenseUrl`

(default: `undefined`)

Specify playready license url.

## Video Binding/Unbinding API

Expand Down
2 changes: 2 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export var hlsDefaultConfig = {
minAutoBitrate: 0, // used by hls
emeEnabled: false, // used by eme-controller
widevineLicenseUrl: undefined, // used by eme-controller
drmSystem: undefined, // used by eme-controller
playreadyLicenseUrl: undefined, // used by eme-controller
requestMediaKeySystemAccessFunc:
requestMediaKeySystemAccess // used by eme-controller
};
Expand Down
125 changes: 84 additions & 41 deletions src/controller/eme-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
import EventHandler from '../event-handler';
import Event from '../events';
import { ErrorTypes, ErrorDetails } from '../errors';

import { base64ToArrayBuffer, buildPlayReadyPSSHBox, makePlayreadyHeaders } from '../utils/eme-helper';
import { logger } from '../utils/logger';

const { XMLHttpRequest } = window;
const { XMLHttpRequest, atob, DOMParser } = window;
Comment thread
ssreed marked this conversation as resolved.
Outdated

const MAX_LICENSE_REQUEST_FAILURES = 3;

Expand All @@ -22,6 +22,11 @@ const KeySystems = {
PLAYREADY: 'com.microsoft.playready'
};

const DRMIdentifiers = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use these values for drmSystem and expose them on public API?

WIDEVINE: 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
PLAYREADY: 'com.microsoft.playready'
};

/**
* @see https://developer.mozilla.org/en-US/docs/Web/API/MediaKeySystemConfiguration
* @param {Array<string>} audioCodecs List of required audio codecs to support
Expand Down Expand Up @@ -67,6 +72,7 @@ const createWidevineMediaKeySystemConfigurations = function (audioCodecs, videoC
const getSupportedMediaKeySystemConfigurations = function (keySystem, audioCodecs, videoCodecs) {
switch (keySystem) {
case KeySystems.WIDEVINE:
case KeySystems.PLAYREADY:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this the same proc, we could rename createWidevineMediaKeySystemConfigurations to reflect it is not widevine specific?

return createWidevineMediaKeySystemConfigurations(audioCodecs, videoCodecs);
default:
throw Error('Unknown key-system: ' + keySystem);
Expand All @@ -88,12 +94,16 @@ class EMEController extends EventHandler {
constructor (hls) {
super(hls,
Event.MEDIA_ATTACHED,
Event.MANIFEST_PARSED
Event.MANIFEST_PARSED,
Event.LEVEL_LOADED,
Event.FRAG_LOADED
);

this._widevineLicenseUrl = hls.config.widevineLicenseUrl;
this._playreadyLicenseUrl = hls.config.playreadyLicenseUrl;
this._licenseXhrSetup = hls.config.licenseXhrSetup;
this._emeEnabled = hls.config.emeEnabled;
this._selectedDrm = hls.config.drmSystem;

this._requestMediaKeySystemAccess = hls.config.requestMediaKeySystemAccessFunc;

Expand All @@ -104,6 +114,9 @@ class EMEController extends EventHandler {
this._isMediaEncrypted = false;

this._requestLicenseFailureCount = 0;

this._initData = null;
Comment thread
ssreed marked this conversation as resolved.
Outdated
this._initDataType = '';
Comment thread
ssreed marked this conversation as resolved.
Outdated
}

/**
Expand All @@ -117,6 +130,9 @@ class EMEController extends EventHandler {
case KeySystems.WIDEVINE:
url = this._widevineLicenseUrl;
break;
case KeySystems.PLAYREADY:
url = this._playreadyLicenseUrl;
break;
default:
url = null;
break;
Expand Down Expand Up @@ -195,7 +211,6 @@ class EMEController extends EventHandler {
mediaKeysListItem.mediaKeys = mediaKeys;

logger.log(`Media-keys created for key-system "${keySystem}"`);

this._onMediaKeysCreated();
})
.catch((err) => {
Expand All @@ -212,6 +227,7 @@ class EMEController extends EventHandler {
this._mediaKeysList.forEach((mediaKeysListItem) => {
if (!mediaKeysListItem.mediaKeysSession) {
mediaKeysListItem.mediaKeysSession = mediaKeysListItem.mediaKeys.createSession();
this._haveKeySession = true;
this._onNewMediaKeySession(mediaKeysListItem.mediaKeysSession);
}
});
Expand Down Expand Up @@ -240,7 +256,6 @@ class EMEController extends EventHandler {

_onMediaEncrypted (initDataType, initData) {
logger.log(`Media is encrypted using "${initDataType}" init data type`);

this._isMediaEncrypted = true;
this._mediaEncryptionInitDataType = initDataType;
this._mediaEncryptionInitData = initData;
Expand Down Expand Up @@ -356,7 +371,7 @@ class EMEController extends EventHandler {

xhr.responseType = 'arraybuffer';
xhr.onreadystatechange =
this._onLicenseRequestReadyStageChange.bind(this, xhr, url, keyMessage, callback);
this._onLicenseRequestReadyStageChange.bind(this, xhr, url, keyMessage, callback);
return xhr;
}

Expand Down Expand Up @@ -404,28 +419,16 @@ class EMEController extends EventHandler {
let challenge;

if (keysListItem.mediaKeySystemDomain === KeySystems.PLAYREADY) {
logger.error('PlayReady is not supported (yet)');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was firmly believing we'd remove this line one day 👍


// from https://github.com/MicrosoftEdge/Demos/blob/master/eme/scripts/demo.js
/*
if (this.licenseType !== this.LICENSE_TYPE_WIDEVINE) {
// For PlayReady CDMs, we need to dig the Challenge out of the XML.
var keyMessageXml = new DOMParser().parseFromString(String.fromCharCode.apply(null, new Uint16Array(keyMessage)), 'application/xml');
if (keyMessageXml.getElementsByTagName('Challenge')[0]) {
challenge = atob(keyMessageXml.getElementsByTagName('Challenge')[0].childNodes[0].nodeValue);
} else {
throw 'Cannot find <Challenge> in key message';
}
var headerNames = keyMessageXml.getElementsByTagName('name');
var headerValues = keyMessageXml.getElementsByTagName('value');
if (headerNames.length !== headerValues.length) {
throw 'Mismatched header <name>/<value> pair in key message';
}
for (var i = 0; i < headerNames.length; i++) {
xhr.setRequestHeader(headerNames[i].childNodes[0].nodeValue, headerValues[i].childNodes[0].nodeValue);
}
}
*/
// For PlayReady CDMs, we need to dig the Challenge out of the XML.
const keyMessageXml = new DOMParser().parseFromString(String.fromCharCode.apply(null, new Uint16Array(keyMessage)), 'application/xml');
const challengeElement = keyMessageXml.querySelector('Challenge');

if (challengeElement) {
challenge = atob(challengeElement.textContent);
} else {
throw Error('Cannot find <Challenge> in key message');
}
} else if (keysListItem.mediaKeySystemDomain === KeySystems.WIDEVINE) {
// For Widevine CDMs, the challenge is the keyMessage.
challenge = keyMessage;
Expand All @@ -452,38 +455,78 @@ class EMEController extends EventHandler {

const url = this.getLicenseServerUrl(keysListItem.mediaKeySystemDomain);
const xhr = this._createLicenseXhr(url, keyMessage, callback);
const challenge = this._generateLicenseRequestChallenge(keysListItem, keyMessage);

if (keysListItem.mediaKeySystemDomain === KeySystems.PLAYREADY) {
const playReadyHeaders = makePlayreadyHeaders(keyMessage);

if (playReadyHeaders.length > 0) {
playReadyHeaders.forEach((header) => {
xhr.setRequestHeader(header.name, header.value);
});
}
}

logger.log(`Sending license request to URL: ${url}`);

xhr.send(this._generateLicenseRequestChallenge(keysListItem, keyMessage));
xhr.send(challenge);
}

onMediaAttached (data) {
if (!this._emeEnabled) {
Comment thread
ssreed marked this conversation as resolved.
Outdated
return;
}

const media = data.media;

// keep reference of media
this._media = media;

// FIXME: also handle detaching media !

media.addEventListener('encrypted', (e) => {
this._onMediaEncrypted(e.initDataType, e.initData);
});
if (!this._hasSetMediaKeys) {
media.addEventListener('encrypted', (e) => {
this._onMediaEncrypted(e.initDataType, e.initData);
});
}
}

onManifestParsed (data) {
if (!this._emeEnabled) {
Comment thread
ssreed marked this conversation as resolved.
Outdated
return;
}

const audioCodecs = data.levels.map((level) => level.audioCodec);
const videoCodecs = data.levels.map((level) => level.videoCodec);

this._attemptKeySystemAccess(KeySystems.WIDEVINE, audioCodecs, videoCodecs);
this._attemptKeySystemAccess(KeySystems[this._selectedDrm], audioCodecs, videoCodecs);
}

onFragLoaded () {
// add initData and type if they are included in playlist
if (this._initData && !this._hasSetMediaKeys && this._haveKeySession) {
this._onMediaEncrypted(this._initDataType, this._initData);
}
}

/**
* @param {object} data
*/
onLevelLoaded (data) {
if (data.details && data.details.drmInfo && data.details.drmInfo.length > 0) {
const drmInfo = data.details.drmInfo;

drmInfo.forEach((levelkey) => {
const details = levelkey.reluri.split(',');
const encoding = details[0];
const pssh = details[1];

if (levelkey.format === DRMIdentifiers[this._selectedDrm]) {
Comment thread
ssreed marked this conversation as resolved.
Outdated
if (encoding.includes('base64')) {
if (DRMIdentifiers[this._selectedDrm] === 'com.microsoft.playready') {
this._initData = buildPlayReadyPSSHBox(base64ToArrayBuffer(pssh)); // Playready is particular about the pssh box, so it needs to be handcrafted.
} else if (DRMIdentifiers[this._selectedDrm] === 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed') {
this._initData = base64ToArrayBuffer(pssh); // Widevine pssh box
} else {
logger.log('not supported');
}
}

this._initDataType = 'cenc';
Comment thread
ssreed marked this conversation as resolved.
Outdated
}
});
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/hls.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ export default class Hls extends Observer {
}

Controller = config.emeController;
if (Controller) {
if (Controller && config.emeEnabled) {
Comment thread
ssreed marked this conversation as resolved.
const emeController = new Controller(this);

/**
Expand Down
11 changes: 8 additions & 3 deletions src/loader/m3u8-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export default class M3U8Parser {
let frag = new Fragment();
let result;
let i;
let drmInfo = [];
Comment thread
ssreed marked this conversation as resolved.

let firstPdtIndex = null;

Expand All @@ -166,7 +167,7 @@ export default class M3U8Parser {
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
const title = (' ' + result[2]).slice(1);
frag.title = title || null;
frag.tagList.push(title ? [ 'INF', duration, title ] : [ 'INF', duration ]);
frag.tagList.push(title ? ['INF', duration, title] : ['INF', duration]);
} else if (result[3]) { // url
if (Number.isFinite(frag.duration)) {
const sn = currentSN++;
Expand Down Expand Up @@ -217,7 +218,7 @@ export default class M3U8Parser {

switch (result[i]) {
case '#':
frag.tagList.push(value2 ? [ value1, value2 ] : [ value1 ]);
frag.tagList.push(value2 ? [value1, value2] : [value1]);
break;
case 'PLAYLIST-TYPE':
level.type = value1.toUpperCase();
Expand Down Expand Up @@ -252,15 +253,18 @@ export default class M3U8Parser {
decryptiv = keyAttrs.hexadecimalInteger('IV');
if (decryptmethod) {
levelkey = new LevelKey();
if ((decrypturi) && (['AES-128', 'SAMPLE-AES', 'SAMPLE-AES-CENC'].indexOf(decryptmethod) >= 0)) {
if ((decrypturi) && (['AES-128', 'SAMPLE-AES', 'SAMPLE-AES-CENC', 'SAMPLE-AES-CTR'].indexOf(decryptmethod) >= 0)) {
levelkey.method = decryptmethod;
// URI to get the key
levelkey.baseuri = baseurl;
levelkey.reluri = decrypturi;
levelkey.key = null;
levelkey.format = keyAttrs.KEYFORMAT;
// Initialization Vector (IV)
levelkey.iv = decryptiv;
}

drmInfo.push(levelkey);
}
break;
case 'START':
Expand Down Expand Up @@ -302,6 +306,7 @@ export default class M3U8Parser {
level.endSN = currentSN - 1;
level.startCC = level.fragments[0] ? level.fragments[0].cc : 0;
level.endCC = cc;
level.drmInfo = drmInfo;

if (!level.initSegment && level.fragments.length) {
// this is a bit lurky but HLS really has no other way to tell us
Expand Down
Loading