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
Feature: add EME ClearKey support #2934
base: master
Are you sure you want to change the base?
Changes from all commits
86c53b7
1bc96a7
076b4cd
75f0061
1662c11
8f7305b
02c7fdd
9f7242d
6e8bfac
aa06043
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,7 +7,7 @@ import { Events } from '../events'; | |
import { ErrorTypes, ErrorDetails } from '../errors'; | ||
|
||
import { logger } from '../utils/logger'; | ||
import { DRMSystemOptions, EMEControllerConfig } from '../config'; | ||
import { DRMSystemOptions, EMEControllerConfig, KeyidValue } from '../config'; | ||
import { KeySystems, MediaKeyFunc } from '../utils/mediakeys-helper'; | ||
import Hls from '../hls'; | ||
import { ComponentAPI } from '../types/component-api'; | ||
|
@@ -56,6 +56,36 @@ const createWidevineMediaKeySystemConfigurations = function ( | |
]; | ||
}; | ||
|
||
const createClearkeyMediaKeySystemConfigurations = function ( | ||
audioCodecs: string[], | ||
videoCodecs: string[] | ||
): MediaKeySystemConfiguration[] { /* jshint ignore:line */ | ||
const baseConfig: MediaKeySystemConfiguration = { | ||
initDataTypes: ['keyids', 'mp4'], | ||
// label: "", | ||
// persistentState: "not-allowed", // or "required" ? | ||
// distinctiveIdentifier: "not-allowed", // or "required" ? | ||
// sessionTypes: ['temporary'], | ||
audioCapabilities: [], // { contentType: 'audio/mp4; codecs="mp4a.40.2"' } | ||
videoCapabilities: [] // { contentType: 'video/mp4; codecs="avc1.42E01E"' } | ||
}; | ||
|
||
audioCodecs.forEach((codec) => { | ||
baseConfig.audioCapabilities!.push({ | ||
contentType: `audio/mp4; codecs="${codec}"` | ||
}); | ||
}); | ||
videoCodecs.forEach((codec) => { | ||
// logger.log(codec); | ||
baseConfig.videoCapabilities!.push({ | ||
contentType: `video/mp4; codecs="${codec}"` | ||
}); | ||
}); | ||
return [ | ||
baseConfig | ||
]; | ||
}; | ||
|
||
/** | ||
* The idea here is to handle key-system (and their respective platforms) specific configuration differences | ||
* in order to work with the local requestMediaKeySystemAccess method. | ||
|
@@ -77,6 +107,8 @@ const getSupportedMediaKeySystemConfigurations = function ( | |
switch (keySystem) { | ||
case KeySystems.WIDEVINE: | ||
return createWidevineMediaKeySystemConfigurations(audioCodecs, videoCodecs, drmSystemOptions); | ||
case KeySystems.CLEARKEY: | ||
return createClearkeyMediaKeySystemConfigurations(audioCodecs, videoCodecs); | ||
default: | ||
throw new Error(`Unknown key-system: ${keySystem}`); | ||
} | ||
|
@@ -102,6 +134,8 @@ class EMEController implements ComponentAPI { | |
private _widevineLicenseUrl?: string; | ||
private _licenseXhrSetup?: (xhr: XMLHttpRequest, url: string) => void; | ||
private _emeEnabled: boolean; | ||
private _clearkeyServerUrl?: string; | ||
private _clearkeyPair: KeyidValue | null; | ||
private _requestMediaKeySystemAccess: MediaKeyFunc | null; | ||
private _drmSystemOptions: DRMSystemOptions; | ||
|
||
|
@@ -124,6 +158,8 @@ class EMEController implements ComponentAPI { | |
this._widevineLicenseUrl = this._config.widevineLicenseUrl; | ||
this._licenseXhrSetup = this._config.licenseXhrSetup; | ||
this._emeEnabled = this._config.emeEnabled; | ||
this._clearkeyServerUrl = this._config.clearkeyServerUrl; | ||
this._clearkeyPair = this._config.clearkeyPair; | ||
this._requestMediaKeySystemAccess = this._config.requestMediaKeySystemAccessFunc; | ||
this._drmSystemOptions = this._config.drmSystemOptions; | ||
|
||
|
@@ -158,6 +194,11 @@ class EMEController implements ComponentAPI { | |
break; | ||
} | ||
return this._widevineLicenseUrl; | ||
case KeySystems.CLEARKEY: | ||
if (!this._clearkeyServerUrl) { | ||
break; | ||
} | ||
return this._clearkeyServerUrl; | ||
} | ||
|
||
throw new Error(`no license server URL configured for key-system "${keySystem}"`); | ||
|
@@ -248,6 +289,89 @@ class EMEController implements ComponentAPI { | |
}); | ||
} | ||
|
||
private _handleMessage (keySession: MediaKeySession, message: ArrayBuffer) { | ||
// If you had a license server, you would make an asynchronous XMLHttpRequest | ||
// with event.message as the body. The response from the server, as a | ||
// Uint8Array, would then be passed to session.update(). | ||
// Instead, we will generate the license synchronously on the client, using | ||
// the hard-coded KEY. | ||
if (!this._clearkeyPair) { | ||
logger.error('Failed to load the keys'); | ||
} | ||
|
||
const license = this._generateLicense(message); | ||
|
||
keySession.update(license).catch( | ||
function (error) { | ||
logger.error('Failed to update the session', error); | ||
} | ||
); | ||
logger.log(`Received license data (length: ${license ? license.byteLength : license}), updating key-session`); | ||
} | ||
|
||
private _generateLicense (message) { | ||
// Parse the clearkey license request. | ||
const request = JSON.parse(new TextDecoder().decode(message)); | ||
type responseFormat = { | ||
kty?: string, | ||
alg?: string, | ||
kid?: string, | ||
k?: string | ||
}; | ||
|
||
const keyarray: responseFormat[] = []; | ||
for (const id of request.kids) { | ||
const decodedBase64 = this._base64ToHex(id); | ||
// logger.log(`decodedBase64: ${decodedBase64}`); | ||
if (!(this._clearkeyPair as KeyidValue).hasOwnProperty(decodedBase64)) { | ||
logger.error('No pair key, please use lower case'); | ||
} | ||
keyarray.push( | ||
{ | ||
kty: 'oct', | ||
alg: 'A128KW', | ||
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. The |
||
kid: id, | ||
k: this._hexToBase64((this._clearkeyPair as KeyidValue)[decodedBase64]) | ||
// k: "aeqoAqZ2Ovl56NGUD7iDkg" | ||
} | ||
); | ||
} | ||
|
||
logger.log(JSON.stringify({ | ||
keys: keyarray, | ||
type: 'temporary' | ||
})); | ||
|
||
return new TextEncoder().encode(JSON.stringify({ | ||
keys: keyarray, | ||
type: 'temporary' | ||
})); | ||
} | ||
|
||
private _hexToBase64 (hexstring) { | ||
var encodedBase64 = btoa(hexstring.match(/\w{2}/g).map(function (a) { | ||
return String.fromCharCode(parseInt(a, 16)); | ||
}).join('')); | ||
|
||
var start = 0; | ||
var end = encodedBase64.length; | ||
while (end > start && encodedBase64[end - 1] === '=') { | ||
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. Need to also handle converting I had come up with these helpers when I was doing my validation.
|
||
--end; | ||
} | ||
return (start > 0 || end < encodedBase64.length) ? encodedBase64.substring(start, end) : encodedBase64; | ||
} | ||
|
||
private _base64ToHex (str) { | ||
const raw = atob(str); | ||
logger.log(raw); | ||
let result = ''; | ||
for (let i = 0; i < raw.length; i++) { | ||
const hex = raw.charCodeAt(i).toString(16); | ||
result += (hex.length === 2 ? hex : '0' + hex); | ||
} | ||
return result.toLowerCase(); | ||
} | ||
|
||
/** | ||
* @private | ||
* @param {*} keySession | ||
|
@@ -268,10 +392,14 @@ class EMEController implements ComponentAPI { | |
private _onKeySessionMessage (keySession: MediaKeySession, message: ArrayBuffer) { | ||
logger.log('Got EME message event, creating license request'); | ||
|
||
this._requestLicense(message, (data: ArrayBuffer) => { | ||
logger.log(`Received license data (length: ${data ? data.byteLength : data}), updating key-session`); | ||
keySession.update(data); | ||
}); | ||
if (this._clearkeyPair && !this._clearkeyServerUrl) { | ||
this._handleMessage(keySession, message); | ||
} else { | ||
this._requestLicense(message, (data: ArrayBuffer) => { | ||
logger.log(`Received license data (length: ${data ? data.byteLength : data}), updating key-session`); | ||
keySession.update(data); | ||
}); | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -501,6 +629,10 @@ class EMEController implements ComponentAPI { | |
case KeySystems.WIDEVINE: | ||
// For Widevine CDMs, the challenge is the keyMessage. | ||
return keyMessage; | ||
case KeySystems.CLEARKEY: | ||
// For CLEARKEY, the challenge is the keyMessage. | ||
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. It's a PSSH box. If we needed to for whatever reason provide the end-user with the contents of the key message, we could parse like this.
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.
https://github.com/video-dev/hls.js/blob/v1.3.0-beta.2/src/utils/mp4-tools.ts#L1108 |
||
return keyMessage; | ||
// return JSON.parse(new TextDecoder().decode(keyMessage)); | ||
} | ||
|
||
throw new Error(`unsupported key-system: ${keysListItem.mediaKeySystemDomain}`); | ||
|
@@ -530,7 +662,14 @@ class EMEController implements ComponentAPI { | |
const xhr = this._createLicenseXhr(url, keyMessage, callback); | ||
logger.log(`Sending license request to URL: ${url}`); | ||
const challenge = this._generateLicenseRequestChallenge(keysListItem, keyMessage); | ||
xhr.send(challenge); | ||
switch (keysListItem.mediaKeySystemDomain) { | ||
case KeySystems.WIDEVINE: | ||
xhr.send(challenge); | ||
case KeySystems.CLEARKEY: | ||
// xhr.setRequestHeader('content-type', 'application/json') | ||
// xhr.send(JSON.stringify(challenge)); | ||
xhr.send(challenge); | ||
} | ||
} catch (e) { | ||
logger.error(`Failure requesting DRM license: ${e}`); | ||
this.hls.trigger(Events.ERROR, { | ||
|
@@ -589,8 +728,11 @@ class EMEController implements ComponentAPI { | |
const videoCodecs = data.levels.map((level) => level.videoCodec).filter( | ||
(videoCodec: string | undefined): videoCodec is string => !!videoCodec | ||
); | ||
|
||
this._attemptKeySystemAccess(KeySystems.WIDEVINE, audioCodecs, videoCodecs); | ||
if (this._clearkeyPair || this._clearkeyServerUrl) { | ||
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. We definitely don't want to always prefer setting up a ClearKey session over any real DRM protection if we get an I think this is potentially why we want to hook the HLS manifest parsing and preemptively setup a ClearKey MediaKeySession when we detect SAMPLE-AES based content. We can request from the user if they want to decrypt using ClearKey. And thats when we are provided with the ClearKey information the user thinks we will need to do playback. |
||
this._attemptKeySystemAccess(KeySystems.CLEARKEY, audioCodecs, videoCodecs); | ||
} else { | ||
this._attemptKeySystemAccess(KeySystems.WIDEVINE, audioCodecs, videoCodecs); | ||
} | ||
} | ||
} | ||
|
||
|
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.
In some browsers; "cenc" is the init data type that will be selected when a
org.w3.clearkey
keysystem is requested.Additionally,
encryptionScheme: 'cbcs'
can be added to further clarify the requestThere 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.
initDataTypes
for all keysystems can be found in utils/mediakeys-helper.ts:https://github.com/video-dev/hls.js/blob/v1.3.0-beta.2/src/utils/mediakeys-helper.ts#L125-L126