New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve EME Controller: Add PlayReady support #1918
Changes from 8 commits
4fb5fdb
b1e50f8
3b423f5
5d2a4ac
fef1409
6713568
294d8af
8236809
d77f6d0
d04049e
685e6df
7c670cc
0401518
00025d1
09d9660
a9d9e8f
1c9021f
56a357f
ca9bcc3
49e22ca
897257d
177b1e7
61494eb
32215cc
e840bfb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,10 +7,10 @@ | |
import EventHandler from '../event-handler'; | ||
import Event from '../events'; | ||
import { ErrorTypes, ErrorDetails } from '../errors'; | ||
|
||
import { base64ToArrayBuffer, buildPlayReadyPSSHBox } from '../utils/eme-helper'; | ||
import { logger } from '../utils/logger'; | ||
|
||
const { XMLHttpRequest } = window; | ||
const { XMLHttpRequest, atob, DOMParser } = window; | ||
ssreed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const MAX_LICENSE_REQUEST_FAILURES = 3; | ||
|
||
|
@@ -22,6 +22,11 @@ const KeySystems = { | |
PLAYREADY: 'com.microsoft.playready' | ||
}; | ||
|
||
const DRMIdentifiers = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not use these values for |
||
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 | ||
|
@@ -67,6 +72,7 @@ const createWidevineMediaKeySystemConfigurations = function (audioCodecs, videoC | |
const getSupportedMediaKeySystemConfigurations = function (keySystem, audioCodecs, videoCodecs) { | ||
switch (keySystem) { | ||
case KeySystems.WIDEVINE: | ||
case KeySystems.PLAYREADY: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if this the same proc, we could rename |
||
return createWidevineMediaKeySystemConfigurations(audioCodecs, videoCodecs); | ||
default: | ||
throw Error('Unknown key-system: ' + keySystem); | ||
|
@@ -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; | ||
|
||
|
@@ -104,6 +114,9 @@ class EMEController extends EventHandler { | |
this._isMediaEncrypted = false; | ||
|
||
this._requestLicenseFailureCount = 0; | ||
|
||
this._initData = null; | ||
ssreed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
this._initDataType = ''; | ||
ssreed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/** | ||
|
@@ -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; | ||
|
@@ -195,7 +211,6 @@ class EMEController extends EventHandler { | |
mediaKeysListItem.mediaKeys = mediaKeys; | ||
|
||
logger.log(`Media-keys created for key-system "${keySystem}"`); | ||
|
||
this._onMediaKeysCreated(); | ||
}) | ||
.catch((err) => { | ||
|
@@ -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); | ||
} | ||
}); | ||
|
@@ -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; | ||
|
@@ -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; | ||
} | ||
|
||
|
@@ -400,32 +415,30 @@ class EMEController extends EventHandler { | |
* @param {ArrayBuffer} keyMessage | ||
* @returns {ArrayBuffer} Challenge data posted to license server | ||
*/ | ||
_generateLicenseRequestChallenge (keysListItem, keyMessage) { | ||
_generateLicenseRequestChallenge (keysListItem, keyMessage, xhr) { | ||
ssreed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let challenge; | ||
|
||
if (keysListItem.mediaKeySystemDomain === KeySystems.PLAYREADY) { | ||
logger.error('PlayReady is not supported (yet)'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'); | ||
|
||
if (keyMessageXml.getElementsByTagName('Challenge')[0]) { | ||
challenge = atob(keyMessageXml.getElementsByTagName('Challenge')[0].childNodes[0].nodeValue); | ||
ssreed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else { | ||
throw Error('Cannot find <Challenge> in key message'); | ||
} | ||
|
||
const headerNames = keyMessageXml.getElementsByTagName('name'); | ||
const headerValues = keyMessageXml.getElementsByTagName('value'); | ||
|
||
if (headerNames.length !== headerValues.length) { | ||
throw Error('Mismatched header <name>/<value> pair in key message'); | ||
} | ||
|
||
for (let i = 0; i < headerNames.length; i++) { | ||
xhr.setRequestHeader(headerNames[i].childNodes[0].nodeValue, headerValues[i].childNodes[0].nodeValue); | ||
} | ||
} else if (keysListItem.mediaKeySystemDomain === KeySystems.WIDEVINE) { | ||
// For Widevine CDMs, the challenge is the keyMessage. | ||
challenge = keyMessage; | ||
|
@@ -455,7 +468,7 @@ class EMEController extends EventHandler { | |
|
||
logger.log(`Sending license request to URL: ${url}`); | ||
|
||
xhr.send(this._generateLicenseRequestChallenge(keysListItem, keyMessage)); | ||
xhr.send(this._generateLicenseRequestChallenge(keysListItem, keyMessage, xhr)); | ||
} | ||
|
||
onMediaAttached (data) { | ||
|
@@ -469,10 +482,11 @@ class EMEController extends EventHandler { | |
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) { | ||
|
@@ -483,7 +497,51 @@ class EMEController extends EventHandler { | |
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 () { | ||
if (!this._emeEnabled) { | ||
return; | ||
} | ||
|
||
// 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 (!this._emeEnabled) { | ||
ssreed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return; | ||
} | ||
|
||
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]) { | ||
ssreed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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'; | ||
ssreed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
}); | ||
} | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?