Skip to content

Commit 6acc9bf

Browse files
author
Bryan Yao
committed
v1.0 clearkey EME
1 parent 7342b03 commit 6acc9bf

File tree

3 files changed

+144
-8
lines changed

3 files changed

+144
-8
lines changed

src/config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,16 @@ export type DRMSystemOptions = {
4444
videoRobustness?: string,
4545
}
4646

47+
export interface KeyidValue {
48+
[keyid: string] : string;
49+
}
50+
4751
export type EMEControllerConfig = {
4852
licenseXhrSetup?: (xhr: XMLHttpRequest, url: string) => void,
4953
emeEnabled: boolean,
5054
widevineLicenseUrl?: string,
55+
clearkeyServerUrl?: string,
56+
clearkeyPair: KeyidValue,
5157
drmSystemOptions: DRMSystemOptions,
5258
requestMediaKeySystemAccessFunc: MediaKeyFunc | null,
5359
};
@@ -244,6 +250,8 @@ export const hlsDefaultConfig: HlsConfig = {
244250
maxLoadingDelay: 4, // used by abr-controller
245251
minAutoBitrate: 0, // used by hls
246252
emeEnabled: false, // used by eme-controller
253+
clearkeyServerUrl: void 0,
254+
clearkeyPair: {},
247255
widevineLicenseUrl: void 0, // used by eme-controller
248256
drmSystemOptions: {}, // used by eme-controller
249257
requestMediaKeySystemAccessFunc: requestMediaKeySystemAccess, // used by eme-controller

src/controller/eme-controller.ts

Lines changed: 135 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Events } from '../events';
77
import { ErrorTypes, ErrorDetails } from '../errors';
88

99
import { logger } from '../utils/logger';
10-
import { DRMSystemOptions, EMEControllerConfig } from '../config';
10+
import { DRMSystemOptions, EMEControllerConfig, KeyidValue } from '../config';
1111
import { KeySystems, MediaKeyFunc } from '../utils/mediakeys-helper';
1212
import Hls from '../hls';
1313
import { ComponentAPI } from '../types/component-api';
@@ -56,6 +56,36 @@ const createWidevineMediaKeySystemConfigurations = function (
5656
];
5757
};
5858

59+
const createClearkeyMediaKeySystemConfigurations = function (
60+
audioCodecs: string[],
61+
videoCodecs: string[],
62+
drmSystemOptions: DRMSystemOptions
63+
): MediaKeySystemConfiguration[] { /* jshint ignore:line */
64+
const baseConfig: MediaKeySystemConfiguration = {
65+
initDataTypes: ['keyids', 'mp4'],
66+
// label: "",
67+
// persistentState: "not-allowed", // or "required" ?
68+
// distinctiveIdentifier: "not-allowed", // or "required" ?
69+
// sessionTypes: ['temporary'],
70+
audioCapabilities: [], // { contentType: 'audio/mp4; codecs="mp4a.40.2"' }
71+
videoCapabilities: [] // { contentType: 'video/mp4; codecs="avc1.42E01E"' }
72+
};
73+
74+
audioCodecs.forEach((codec) => {
75+
logger.log(codec);
76+
});
77+
78+
videoCodecs.forEach((codec) => {
79+
// logger.log(codec);
80+
baseConfig.videoCapabilities!.push({
81+
contentType: `video/mp4; codecs="${codec}"`
82+
});
83+
});
84+
return [
85+
baseConfig
86+
];
87+
};
88+
5989
/**
6090
* The idea here is to handle key-system (and their respective platforms) specific configuration differences
6191
* in order to work with the local requestMediaKeySystemAccess method.
@@ -75,8 +105,10 @@ const getSupportedMediaKeySystemConfigurations = function (
75105
drmSystemOptions: DRMSystemOptions
76106
): MediaKeySystemConfiguration[] {
77107
switch (keySystem) {
78-
case KeySystems.WIDEVINE:
79-
return createWidevineMediaKeySystemConfigurations(audioCodecs, videoCodecs, drmSystemOptions);
108+
case KeySystems.WIDEVINE:
109+
return createWidevineMediaKeySystemConfigurations(audioCodecs, videoCodecs, drmSystemOptions);
110+
case KeySystems.CLEARKEY:
111+
return createClearkeyMediaKeySystemConfigurations(audioCodecs, videoCodecs, drmSystemOptions);
80112
default:
81113
throw new Error(`Unknown key-system: ${keySystem}`);
82114
}
@@ -102,6 +134,8 @@ class EMEController implements ComponentAPI {
102134
private _widevineLicenseUrl?: string;
103135
private _licenseXhrSetup?: (xhr: XMLHttpRequest, url: string) => void;
104136
private _emeEnabled: boolean;
137+
private _clearkeyServerUrl?: string;
138+
private _clearkeyPair: KeyidValue;
105139
private _requestMediaKeySystemAccess: MediaKeyFunc | null;
106140
private _drmSystemOptions: DRMSystemOptions;
107141

@@ -124,6 +158,8 @@ class EMEController implements ComponentAPI {
124158
this._widevineLicenseUrl = this._config.widevineLicenseUrl;
125159
this._licenseXhrSetup = this._config.licenseXhrSetup;
126160
this._emeEnabled = this._config.emeEnabled;
161+
this._clearkeyServerUrl = this._config.clearkeyServerUrl;
162+
this._clearkeyPair = this._config.clearkeyPair;
127163
this._requestMediaKeySystemAccess = this._config.requestMediaKeySystemAccessFunc;
128164
this._drmSystemOptions = this._config.drmSystemOptions;
129165

@@ -248,6 +284,89 @@ class EMEController implements ComponentAPI {
248284
});
249285
}
250286

287+
private _handleMessage (keySession: MediaKeySession, message: ArrayBuffer) {
288+
// If you had a license server, you would make an asynchronous XMLHttpRequest
289+
// with event.message as the body. The response from the server, as a
290+
// Uint8Array, would then be passed to session.update().
291+
// Instead, we will generate the license synchronously on the client, using
292+
// the hard-coded KEY.
293+
if (this._clearkeyPair == null) {
294+
logger.error('Failed to load the keys');
295+
}
296+
297+
let license = this._generateLicense(message);
298+
299+
keySession.update(license).catch(
300+
function (error) {
301+
logger.error('Failed to update the session', error);
302+
}
303+
);
304+
logger.log(`Received license data (length: ${license ? license.byteLength : license}), updating key-session`);
305+
}
306+
307+
private _generateLicense (message) {
308+
// Parse the clearkey license request.
309+
let request = JSON.parse(new TextDecoder().decode(message));
310+
type responseFormat = {
311+
kty?: string,
312+
alg?: string,
313+
kid?: string,
314+
k?: string
315+
}
316+
317+
let keyarray: responseFormat[] = [];
318+
for (let id of request.kids) {
319+
let decodedBase64 = this.base64ToHex(id);
320+
// logger.log(`decodedBase64: ${decodedBase64}`);
321+
if (!this._clearkeyPair.hasOwnProperty(decodedBase64)) {
322+
logger.error('No pair key, please use lower case');
323+
}
324+
keyarray.push(
325+
{
326+
kty: 'oct',
327+
alg: 'A128KW',
328+
kid: id,
329+
k: this.hexToBase64(this._clearkeyPair[decodedBase64])
330+
// k: "aeqoAqZ2Ovl56NGUD7iDkg"
331+
}
332+
);
333+
}
334+
335+
logger.log(JSON.stringify({
336+
keys: keyarray,
337+
type: 'temporary'
338+
}));
339+
340+
return new TextEncoder().encode(JSON.stringify({
341+
keys: keyarray,
342+
type: 'temporary'
343+
}));
344+
}
345+
346+
private hexToBase64 (hexstring) {
347+
var encodedBase64 = btoa(hexstring.match(/\w{2}/g).map(function (a) {
348+
return String.fromCharCode(parseInt(a, 16));
349+
}).join(''));
350+
351+
var start = 0;
352+
var end = encodedBase64.length;
353+
while (end > start && encodedBase64[end - 1] === '=') {
354+
--end;
355+
}
356+
return (start > 0 || end < encodedBase64.length) ? encodedBase64.substring(start, end) : encodedBase64;
357+
}
358+
359+
private base64ToHex (str) {
360+
const raw = atob(str);
361+
logger.log(raw);
362+
let result = '';
363+
for (let i = 0; i < raw.length; i++) {
364+
const hex = raw.charCodeAt(i).toString(16);
365+
result += (hex.length === 2 ? hex : '0' + hex);
366+
}
367+
return result.toLowerCase();
368+
}
369+
251370
/**
252371
* @private
253372
* @param {*} keySession
@@ -268,10 +387,14 @@ class EMEController implements ComponentAPI {
268387
private _onKeySessionMessage (keySession: MediaKeySession, message: ArrayBuffer) {
269388
logger.log('Got EME message event, creating license request');
270389

271-
this._requestLicense(message, (data: ArrayBuffer) => {
272-
logger.log(`Received license data (length: ${data ? data.byteLength : data}), updating key-session`);
273-
keySession.update(data);
274-
});
390+
if (this._clearkeyPair && this._clearkeyServerUrl === void 0) {
391+
this._handleMessage(keySession, message);
392+
} else {
393+
this._requestLicense(message, (data: ArrayBuffer) => {
394+
logger.log(`Received license data (length: ${data ? data.byteLength : data}), updating key-session`);
395+
keySession.update(data);
396+
});
397+
}
275398
}
276399

277400
/**
@@ -590,7 +713,11 @@ class EMEController implements ComponentAPI {
590713
(videoCodec: string | undefined): videoCodec is string => !!videoCodec
591714
);
592715

593-
this._attemptKeySystemAccess(KeySystems.WIDEVINE, audioCodecs, videoCodecs);
716+
if (this._clearkeyPair || this._clearkeyServerUrl) {
717+
this._attemptKeySystemAccess(KeySystems.CLEARKEY, audioCodecs, videoCodecs);
718+
} else {
719+
this._attemptKeySystemAccess(KeySystems.WIDEVINE, audioCodecs, videoCodecs);
720+
}
594721
}
595722
}
596723

src/utils/mediakeys-helper.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
export enum KeySystems {
55
WIDEVINE = 'com.widevine.alpha',
66
PLAYREADY = 'com.microsoft.playready',
7+
CLEARKEY = 'org.w3.clearkey'
78
}
89

910
export type MediaKeyFunc = (keySystem: KeySystems, supportedConfigurations: MediaKeySystemConfiguration[]) => Promise<MediaKeySystemAccess>;

0 commit comments

Comments
 (0)