Skip to content

Commit

Permalink
fix: use in-spec EME for versions of Safari which support it
Browse files Browse the repository at this point in the history
based on #87
  • Loading branch information
brandonocasey committed Sep 30, 2021
1 parent a794ea9 commit bf5ffd2
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 50 deletions.
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
84 changes: 64 additions & 20 deletions src/eme.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ 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 } from './fairplay';

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

/**
* Returns an array of MediaKeySystemConfigurationObjects provided in the keySystem
Expand All @@ -15,16 +19,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 +61,10 @@ export const getSupportedConfigurations = (keySystemOptions) => {
supportedConfiguration.persistentState = persistentState;
}

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

return [supportedConfiguration];
};

Expand All @@ -62,7 +75,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 Down Expand Up @@ -292,10 +305,10 @@ export const defaultGetLicense = (keySystemOptions) => (emeOptions, keyMessage,
}, httpResponseHandler(callback, true));
};

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

resolve(license);
});
};

if (isFairplayKeySystem(keySystem)) {
getLicenseFn(emeOptions, null, keyMessage, callback);
} else {
getLicenseFn(emeOptions, keyMessage, callback);
}
});
};
};
Expand All @@ -315,14 +334,32 @@ 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.`);
}

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

const isFairplay = isFairplayKeySystem(keySystem);

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);
}
}

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

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

if (typeof video.mediaKeysObject === 'undefined') {
// Prevent entering this path again.
Expand All @@ -351,11 +389,11 @@ export const standard5July2016 = ({

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

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

if (!keySystemOptions.getCertificate) {
Expand Down Expand Up @@ -392,18 +430,24 @@ export const standard5July2016 = ({
}

return keySystemPromise.then(() => {
const {getLicense} = standardizeKeySystemOptions(
video.keySystem,
options.keySystems[video.keySystem]
);
let getLicenseFn = null;

// if key system has not been determined then addSession doesn't need getLicense
if (video.keySystem) {
const {getLicense} = standardizeKeySystemOptions(
keySystem,
options.keySystems[video.keySystem]
);

getLicenseFn = promisifyGetLicense(keySystem, getLicense, eventBus);
}

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: getLicenseFn,
removeSession,
eventBus
});
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
59 changes: 52 additions & 7 deletions test/eme.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import QUnit from 'qunit';
import videojs from 'video.js';
import window from 'global/window';
import {
defaultGetLicense,
standard5July2016,
Expand Down Expand Up @@ -573,7 +574,7 @@ if (!videojs.browser.IS_ANY_SAFARI) {
});
}

QUnit.test('errors when neither url nor getLicense is given', function(assert) {
QUnit.test('errors when missing url/licenseUri or getLicense', function(assert) {
const options = {
keySystems: {
'com.widevine.alpha': {}
Expand All @@ -592,7 +593,32 @@ QUnit.test('errors when neither url nor getLicense is given', function(assert) {
}).catch((err) => {
assert.equal(
err,
'Error: Neither URL nor getLicense function provided to get license',
'Error: Missing url/licenseUri or getLicense in com.widevine.alpha keySystem configuration.',
'correct error message'
);
done();
});
});

QUnit.test('errors when missing certificateUri and getCertificate for fairplay', function(assert) {
const options = {
keySystems: {
'com.apple.fps': {url: 'fake-url'}
}
};
const keySystemAccess = {
keySystem: 'com.apple.fps'
};
const done = assert.async();

standard5July2016({
video: {},
keySystemAccess,
options
}).catch((err) => {
assert.equal(
err,
'Error: Missing getCertificate or certificateUri in com.apple.fps keySystem configuration.',
'correct error message'
);
done();
Expand Down Expand Up @@ -1087,6 +1113,25 @@ QUnit.test('licenseHeaders keySystems property overrides emeHeaders value', func
});
});

QUnit.test('sets required fairplay defaults if not explicitly configured', function(assert) {
const origRequestMediaKeySystemAccess = window.navigator.requestMediaKeySystemAccess;

window.navigator.requestMediaKeySystemAccess = (keySystem, systemOptions) => {
assert.ok(
systemOptions[0].initDataTypes.indexOf('sinf') !== -1,
'includes required initDataType'
);
assert.ok(
systemOptions[0].videoCapabilities[0].contentType.indexOf('video/mp4') !== -1,
'includes required video contentType'
);
};

getSupportedKeySystem({'com.apple.fps': {}});

window.requestMediaKeySystemAccess = origRequestMediaKeySystemAccess;
});

QUnit.module('session management');

QUnit.test('addSession saves options', function(assert) {
Expand Down Expand Up @@ -1190,7 +1235,7 @@ QUnit.module('videojs-contrib-eme getSupportedConfigurations');

QUnit.test('includes audio and video content types', function(assert) {
assert.deepEqual(
getSupportedConfigurations({
getSupportedConfigurations('com.widevine.alpha', {
audioContentType: 'audio/mp4; codecs="mp4a.40.2"',
videoContentType: 'video/mp4; codecs="avc1.42E01E"'
}),
Expand All @@ -1208,7 +1253,7 @@ QUnit.test('includes audio and video content types', function(assert) {

QUnit.test('includes audio and video robustness', function(assert) {
assert.deepEqual(
getSupportedConfigurations({
getSupportedConfigurations('com.widevine.alpha', {
audioRobustness: 'SW_SECURE_CRYPTO',
videoRobustness: 'SW_SECURE_CRYPTO'
}),
Expand All @@ -1226,7 +1271,7 @@ QUnit.test('includes audio and video robustness', function(assert) {

QUnit.test('includes audio and video content types and robustness', function(assert) {
assert.deepEqual(
getSupportedConfigurations({
getSupportedConfigurations('com.widevine.alpha', {
audioContentType: 'audio/mp4; codecs="mp4a.40.2"',
audioRobustness: 'SW_SECURE_CRYPTO',
videoContentType: 'video/mp4; codecs="avc1.42E01E"',
Expand All @@ -1248,15 +1293,15 @@ QUnit.test('includes audio and video content types and robustness', function(ass

QUnit.test('includes persistentState', function(assert) {
assert.deepEqual(
getSupportedConfigurations({ persistentState: 'optional' }),
getSupportedConfigurations('com.widevine.alpha', { persistentState: 'optional' }),
[{ persistentState: 'optional' }],
'included persistentState'
);
});

QUnit.test('uses supportedConfigurations directly if provided', function(assert) {
assert.deepEqual(
getSupportedConfigurations({
getSupportedConfigurations('com.widevine.alpha', {
supportedConfigurations: [{
initDataTypes: ['cenc'],
audioCapabilities: [{
Expand Down
Loading

0 comments on commit bf5ffd2

Please sign in to comment.