Skip to content
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

fix: use in-spec EME for versions of Safari which support it #142

Merged
merged 11 commits into from
Oct 15, 2021
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ player.src({
},
keySystems: {
'org.w3.clearkey': {
initDataTypes: ['cenc', 'webm'],
audioContentType: 'audio/webm; codecs="vorbis"',
videoContentType: 'video/webm; codecs="vp9"',
getCertificate: function(emeOptions, callback) {
Expand Down
140 changes: 97 additions & 43 deletions src/eme.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import { requestPlayreadyLicense } from './playready';
import window from 'global/window';
import {mergeAndRemoveNull} from './utils';
import {httpResponseHandler} from './http-handler.js';
import {
defaultGetCertificate as defaultFairplayGetCertificate,
defaultGetLicense as defaultFairplayGetLicense,
defaultGetContentId as defaultFairplayGetContentId
} from './fairplay';

const isFairplayKeySystem = (str) => str.startsWith('com.apple.fps');

/**
* Returns an array of MediaKeySystemConfigurationObjects provided in the keySystem
Expand All @@ -15,16 +22,21 @@ import {httpResponseHandler} from './http-handler.js';
* @return {Object[]}
* Array of MediaKeySystemConfigurationObjects
*/
export const getSupportedConfigurations = (keySystemOptions) => {
export const getSupportedConfigurations = (keySystem, keySystemOptions) => {
if (keySystemOptions.supportedConfigurations) {
return keySystemOptions.supportedConfigurations;
}

// TODO use initDataTypes when appropriate
const isFairplay = isFairplayKeySystem(keySystem);
const supportedConfiguration = {};
const initDataTypes = keySystemOptions.initDataTypes ||
// fairplay requires an explicit initDataTypes
(isFairplay ? ['sinf'] : null);
const audioContentType = keySystemOptions.audioContentType;
const audioRobustness = keySystemOptions.audioRobustness;
const videoContentType = keySystemOptions.videoContentType;
const videoContentType = keySystemOptions.videoContentType ||
// fairplay requires an explicit videoCapabilities/videoContentType
(isFairplay ? 'video/mp4' : null);
const videoRobustness = keySystemOptions.videoRobustness;
const persistentState = keySystemOptions.persistentState;

Expand Down Expand Up @@ -52,6 +64,10 @@ export const getSupportedConfigurations = (keySystemOptions) => {
supportedConfiguration.persistentState = persistentState;
}

if (initDataTypes) {
supportedConfiguration.initDataTypes = initDataTypes;
}

return [supportedConfiguration];
};

Expand All @@ -62,7 +78,7 @@ export const getSupportedKeySystem = (keySystems) => {
let promise;

Object.keys(keySystems).forEach((keySystem) => {
const supportedConfigurations = getSupportedConfigurations(keySystems[keySystem]);
const supportedConfigurations = getSupportedConfigurations(keySystem, keySystems[keySystem]);

if (!promise) {
promise =
Expand All @@ -84,8 +100,10 @@ export const makeNewRequest = (requestOptions) => {
options,
getLicense,
removeSession,
eventBus
eventBus,
contentId
} = requestOptions;

const keySession = mediaKeys.createSession();

eventBus.trigger('keysessioncreated');
Expand All @@ -97,7 +115,8 @@ export const makeNewRequest = (requestOptions) => {
if (event.messageType !== 'license-request' && event.messageType !== 'license-renewal') {
return;
}
getLicense(options, event.message)

getLicense(options, event.message, contentId)
.then((license) => {
resolve(keySession.update(license));
})
Expand Down Expand Up @@ -192,29 +211,27 @@ export const addSession = ({
initData,
options,
getLicense,
contentId,
removeSession,
eventBus
}) => {
if (video.mediaKeysObject) {
return makeNewRequest({
mediaKeys: video.mediaKeysObject,
initDataType,
initData,
options,
getLicense,
removeSession,
eventBus
});
}

video.pendingSessionData.push({
const sessionData = {
initDataType,
initData,
options,
getLicense,
removeSession,
eventBus
});
eventBus,
contentId
};

if (video.mediaKeysObject) {
sessionData.mediaKeys = video.mediaKeysObject;
return makeNewRequest(sessionData);
}

video.pendingSessionData.push(sessionData);

return Promise.resolve();
};

Expand Down Expand Up @@ -261,7 +278,8 @@ export const addPendingSessions = ({
options: data.options,
getLicense: data.getLicense,
removeSession: data.removeSession,
eventBus: data.eventBus
eventBus: data.eventBus,
contentId: data.contentId
}));
}

Expand Down Expand Up @@ -292,10 +310,10 @@ export const defaultGetLicense = (keySystemOptions) => (emeOptions, keyMessage,
}, httpResponseHandler(callback, true));
};

const promisifyGetLicense = (getLicenseFn, eventBus) => {
return (emeOptions, keyMessage) => {
const promisifyGetLicense = (keySystem, getLicenseFn, eventBus) => {
return (emeOptions, keyMessage, contentId) => {
return new Promise((resolve, reject) => {
getLicenseFn(emeOptions, keyMessage, (err, license) => {
const callback = function(err, license) {
if (eventBus) {
eventBus.trigger('licenserequestattempted');
}
Expand All @@ -305,7 +323,13 @@ const promisifyGetLicense = (getLicenseFn, eventBus) => {
}

resolve(license);
});
};

if (isFairplayKeySystem(keySystem)) {
getLicenseFn(emeOptions, contentId, new Uint8Array(keyMessage), callback);
} else {
getLicenseFn(emeOptions, keyMessage, callback);
}
});
};
};
Expand All @@ -315,14 +339,36 @@ const standardizeKeySystemOptions = (keySystem, keySystemOptions) => {
keySystemOptions = { url: keySystemOptions };
}

if (!keySystemOptions.url && keySystemOptions.licenseUri) {
keySystemOptions.url = keySystemOptions.licenseUri;
}

if (!keySystemOptions.url && !keySystemOptions.getLicense) {
throw new Error('Neither URL nor getLicense function provided to get license');
throw new Error(`Missing url/licenseUri or getLicense in ${keySystem} keySystem configuration.`);
}

const isFairplay = isFairplayKeySystem(keySystem);

if (isFairplay && keySystemOptions.certificateUri && !keySystemOptions.getCertificate) {
keySystemOptions.getCertificate = defaultFairplayGetCertificate(keySystemOptions);
}

if (isFairplay && !keySystemOptions.getCertificate) {
throw new Error(`Missing getCertificate or certificateUri in ${keySystem} keySystem configuration.`);
}

if (isFairplay && !keySystemOptions.getContentId) {
keySystemOptions.getContentId = defaultFairplayGetContentId;
}

if (keySystemOptions.url && !keySystemOptions.getLicense) {
keySystemOptions.getLicense = keySystem === 'com.microsoft.playready' ?
defaultPlayreadyGetLicense(keySystemOptions) :
defaultGetLicense(keySystemOptions);
if (keySystem === 'com.microsoft.playready') {
keySystemOptions.getLicense = defaultPlayreadyGetLicense(keySystemOptions);
} else if (isFairplay) {
keySystemOptions.getLicense = defaultFairplayGetLicense(keySystemOptions);
} else {
keySystemOptions.getLicense = defaultGetLicense(keySystemOptions);
}
}

return keySystemOptions;
Expand All @@ -338,6 +384,21 @@ export const standard5July2016 = ({
eventBus
}) => {
let keySystemPromise = Promise.resolve();
const keySystem = keySystemAccess.keySystem;
let keySystemOptions;

// try catch so that we return a promise rejection
try {
keySystemOptions = standardizeKeySystemOptions(
keySystem,
options.keySystems[keySystem]
);
} catch (e) {
return Promise.reject(e);
}

const contentId = keySystemOptions.getContentId ?
keySystemOptions.getContentId(options, initData) : null;

if (typeof video.mediaKeysObject === 'undefined') {
// Prevent entering this path again.
Expand All @@ -347,16 +408,10 @@ export const standard5July2016 = ({
video.pendingSessionData = [];

let certificate;
let keySystemOptions;

keySystemPromise = new Promise((resolve, reject) => {
// save key system for adding sessions
video.keySystem = keySystemAccess.keySystem;

keySystemOptions = standardizeKeySystemOptions(
keySystemAccess.keySystem,
options.keySystems[keySystemAccess.keySystem]
);
video.keySystem = keySystem;

if (!keySystemOptions.getCertificate) {
resolve(keySystemAccess);
Expand Down Expand Up @@ -392,18 +447,17 @@ export const standard5July2016 = ({
}

return keySystemPromise.then(() => {
const {getLicense} = standardizeKeySystemOptions(
video.keySystem,
options.keySystems[video.keySystem]
);
// if key system has not been determined then addSession doesn't need getLicense
const getLicense = video.keySystem ?
promisifyGetLicense(keySystem, keySystemOptions.getLicense, eventBus) : null;

return addSession({
video,
initDataType,
initData,
options,
// if key system has not been determined then addSession doesn't need getLicense
getLicense: video.keySystem ? promisifyGetLicense(getLicense, eventBus) : null,
getLicense,
contentId,
removeSession,
eventBus
});
Expand Down
2 changes: 1 addition & 1 deletion src/fairplay.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export const defaultGetCertificate = (fairplayOptions) => {
};
};

const defaultGetContentId = (emeOptions, initData) => {
export const defaultGetContentId = (emeOptions, initData) => {
return getHostnameFromUri(uint8ArrayToString(initData));
};

Expand Down
29 changes: 14 additions & 15 deletions src/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,18 @@ const onPlayerReady = (player, emeError) => {

setupSessions(player);

if (window.WebKitMediaKeys) {
if (window.MediaKeys) {
// Support EME 05 July 2016
// Chrome 42+, Firefox 47+, Edge, Safari 12.1+ on macOS 10.14+
player.tech_.el_.addEventListener('encrypted', (event) => {
// TODO convert to videojs.log.debug and add back in
// https://github.com/videojs/video.js/pull/4780
// videojs.log('eme', 'Received an \'encrypted\' event');
setupSessions(player);
handleEncryptedEvent(event, getOptions(player), player.eme.sessions, player.tech_)
.catch(emeError);
});
} else if (window.WebKitMediaKeys) {
const handleFn = (event) => {
// TODO convert to videojs.log.debug and add back in
// https://github.com/videojs/video.js/pull/4780
Expand Down Expand Up @@ -278,18 +289,6 @@ const onPlayerReady = (player, emeError) => {
}
});

} else if (window.MediaKeys) {
// Support EME 05 July 2016
// Chrome 42+, Firefox 47+, Edge, Safari 12.1+ on macOS 10.14+
player.tech_.el_.addEventListener('encrypted', (event) => {
// TODO convert to videojs.log.debug and add back in
// https://github.com/videojs/video.js/pull/4780
// videojs.log('eme', 'Received an \'encrypted\' event');
setupSessions(player);
handleEncryptedEvent(event, getOptions(player), player.eme.sessions, player.tech_)
.catch(emeError);
});

} else if (window.MSMediaKeys) {
// IE11 Windows 8.1+
// Since IE11 doesn't support promises, we have to use a combination of
Expand Down Expand Up @@ -364,7 +363,7 @@ const eme = function(options = {}) {

setupSessions(player);

if (player.tech_.el_.setMediaKeys) {
if (window.MediaKeys) {
handleEncryptedEvent(mockEncryptedEvent, mergedEmeOptions, player.eme.sessions, player.tech_)
.then(() => callback())
.catch((error) => {
Expand All @@ -373,7 +372,7 @@ const eme = function(options = {}) {
emeError(error);
}
});
} else if (player.tech_.el_.msSetMediaKeys) {
} else if (window.MSMediaKeys) {
const msKeyHandler = (event) => {
player.tech_.off('mskeyadded', msKeyHandler);
player.tech_.off('mskeyerror', msKeyHandler);
Expand Down
2 changes: 1 addition & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const stringToUint16Array = (string) => {
};

export const uint8ArrayToString = (array) => {
return String.fromCharCode.apply(null, new Uint16Array(array.buffer));
return String.fromCharCode.apply(null, new Uint8Array(array.buffer || array));
};

export const getHostnameFromUri = (uri) => {
Expand Down
Loading