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 #87

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,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
76 changes: 56 additions & 20 deletions src/eme.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import videojs from 'video.js';
import { requestPlayreadyLicense } from './playready';
import window from 'global/window';
import {mergeAndRemoveNull} from './utils';
import {defaultGetCertificate as defaultFairplayGetCertificate,
defaultGetLicense as defaultFairplayGetLicense } from './fairplay';

export const getSupportedKeySystem = (keySystems) => {
// As this happens after the src is set on the video, we rely only on the set src (we
Expand All @@ -10,10 +12,14 @@ export const getSupportedKeySystem = (keySystems) => {
let promise;

Object.keys(keySystems).forEach((keySystem) => {
// TODO use initDataTypes when appropriate
const systemOptions = {};
const initDataTypes = keySystems[keySystem].initDataTypes ||
// fairplay requires an explicit initDataTypes
(keySystem.startsWith('com.apple.fps') ? ['sinf'] : null);
const audioContentType = keySystems[keySystem].audioContentType;
const videoContentType = keySystems[keySystem].videoContentType;
const videoContentType = keySystems[keySystem].videoContentType ||
// fairplay requires an explicit videoCapabilities
(keySystem.startsWith('com.apple.fps') ? 'video/mp4' : null);

if (audioContentType) {
systemOptions.audioCapabilities = [{
Expand All @@ -25,6 +31,9 @@ export const getSupportedKeySystem = (keySystems) => {
contentType: videoContentType
}];
}
if (initDataTypes) {
systemOptions.initDataTypes = initDataTypes;
}

if (!promise) {
promise = window.navigator.requestMediaKeySystemAccess(keySystem, [systemOptions]);
Expand All @@ -49,7 +58,6 @@ export const makeNewRequest = ({
const keySession = mediaKeys.createSession();

return new Promise((resolve, reject) => {

keySession.addEventListener('message', (event) => {
getLicense(options, event.message)
.then((license) => {
Expand Down Expand Up @@ -199,10 +207,10 @@ const defaultGetLicense = (keySystemOptions) => (emeOptions, keyMessage, callbac
});
};

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

resolve(license);
});
};

if (keySystem.startsWith('com.apple.fps')) {
getLicenseFn(emeOptions, null, keyMessage, callback);
} else {
getLicenseFn(emeOptions, keyMessage, callback);
}
});
};
};
Expand All @@ -221,15 +235,30 @@ const standardizeKeySystemOptions = (keySystem, keySystemOptions) => {
if (typeof keySystemOptions === 'string') {
keySystemOptions = { url: keySystemOptions };
}
if (typeof keySystemOptions.licenseUri !== 'undefined') {
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 configuration: one of url, licenseUri, or getLicense is required');
}

if (typeof keySystemOptions.certificateUri !== 'undefined') {
keySystemOptions.getCertificate = defaultFairplayGetCertificate(keySystemOptions);
}

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 (keySystem.startsWith('com.apple.fps')) {
misteroneill marked this conversation as resolved.
Show resolved Hide resolved
keySystemOptions.getLicense = defaultFairplayGetLicense(keySystemOptions);
} else {
keySystemOptions.getLicense = defaultGetLicense(keySystemOptions);
}
}

if (keySystem.startsWith('com.apple.fps') && !keySystemOptions.getCertificate) {
throw new Error('Missing configuration: one of certificateUri or getCertificate is required');
}

return keySystemOptions;
Expand All @@ -245,6 +274,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 @@ -258,14 +288,14 @@ 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) {
resolve(keySystemAccess);
resolve();
return;
}

Expand All @@ -287,7 +317,7 @@ export const standard5July2016 = ({
certificate,
createdMediaKeys,
options,
getLicense: promisifyGetLicense(keySystemOptions.getLicense, eventBus),
getLicense: promisifyGetLicense(keySystem, keySystemOptions.getLicense, eventBus),
removeSession,
eventBus
});
Expand All @@ -302,16 +332,22 @@ export const standard5July2016 = ({
}

return keySystemPromise.then(() => {
let getLicenseFn;

// addSession only needs getLicense if a key system has been determined
if (video.keySystem) {
getLicenseFn = standardizeKeySystemOptions(keySystem,
options.keySystems[keySystem]).getLicense;
// promisify the function
getLicenseFn = promisifyGetLicense(keySystem, getLicenseFn, eventBus);
}

return addSession({
video,
initDataType,
initData,
options,
// if key system has not been determined then addSession doesn't need getLicense
getLicense: video.keySystem ?
promisifyGetLicense(standardizeKeySystemOptions(
video.keySystem,
options.keySystems[video.keySystem]).getLicense, eventBus) : null,
getLicense: getLicenseFn,
removeSession,
eventBus
});
Expand Down
30 changes: 15 additions & 15 deletions src/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,19 @@ const onPlayerReady = (player, emeError) => {

setupSessions(player);

if (window.WebKitMediaKeys) {
if (window.MediaKeys) {
// Support EME 05 July 2016
// Chrome 42+, Firefox 47+, Edge
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) {
// Support Safari EME with FairPlay
// (also used in early Chrome or Chrome with EME disabled flag)
player.tech_.el_.addEventListener('webkitneedkey', (event) => {
Expand All @@ -228,18 +240,6 @@ const onPlayerReady = (player, emeError) => {
.catch(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 @@ -314,7 +314,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 @@ -323,7 +323,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
45 changes: 43 additions & 2 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 {
standard5July2016,
makeNewRequest,
Expand Down Expand Up @@ -505,7 +506,7 @@ if (!videojs.browser.IS_ANY_SAFARI) {
});
}

QUnit.test('errors when neither url nor getLicense is given', function(assert) {
QUnit.test('errors when none of url, licenseUri, or getLicense is given', function(assert) {
const options = {
keySystems: {
'com.widevine.alpha': {}
Expand All @@ -523,7 +524,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 configuration: one of url, licenseUri, or getLicense is required',
'correct error message'
);
done();
});
});

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

standard5July2016({
video: {},
keySystemAccess,
options
}).catch((err) => {
assert.equal(
err,
'Error: Missing configuration: one of certificateUri or getCertificate is required',
'correct error message'
);
done();
Expand Down Expand Up @@ -888,3 +914,18 @@ QUnit.test('licenseHeaders keySystems property overrides emeHeaders value', func
done();
});
});

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