Skip to content

Commit

Permalink
fix: support in-spec EME for Safaris which support it
Browse files Browse the repository at this point in the history
  • Loading branch information
squarebracket committed May 7, 2019
1 parent 36d5f9c commit 3a6218f
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 64 deletions.
67 changes: 50 additions & 17 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 @@ -12,6 +14,7 @@ export const getSupportedKeySystem = (keySystems) => {
Object.keys(keySystems).forEach((keySystem) => {
// TODO use initDataTypes when appropriate
const systemOptions = {};
const initDataTypes = keySystems[keySystem].initDataTypes;
const audioContentType = keySystems[keySystem].audioContentType;
const videoContentType = keySystems[keySystem].videoContentType;

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

if (!promise) {
promise = window.navigator.requestMediaKeySystemAccess(keySystem, [systemOptions]);
Expand All @@ -49,7 +55,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 +204,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 +217,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 +232,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');
}

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')) {
keySystemOptions.getLicense = defaultFairplayGetLicense(keySystemOptions);
} else {
keySystemOptions.getLicense = defaultGetLicense(keySystemOptions);
}
}

if (keySystem.startsWith('com.apple.fps') && !keySystemOptions.getCertificate) {
throw new Error('Neither URL nor getCertificate provided');
}

return keySystemOptions;
Expand All @@ -245,6 +271,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 +285,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 +314,7 @@ export const standard5July2016 = ({
certificate,
createdMediaKeys,
options,
getLicense: promisifyGetLicense(keySystemOptions.getLicense, eventBus),
getLicense: promisifyGetLicense(keySystem, keySystemOptions.getLicense, eventBus),
removeSession,
eventBus
});
Expand All @@ -302,16 +329,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
95 changes: 48 additions & 47 deletions src/plugin.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import window from 'global/window';
import videojs from 'video.js';
import { standard5July2016, getSupportedKeySystem } from './eme';
import {
Expand Down Expand Up @@ -212,56 +213,56 @@ const onPlayerReady = (player, emeError) => {

setupSessions(player);

// 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);
});
// Support Safari EME with FairPlay
// (also used in early Chrome or Chrome with EME disabled flag)
player.tech_.el_.addEventListener('webkitneedkey', (event) => {
// TODO convert to videojs.log.debug and add back in
// https://github.com/videojs/video.js/pull/4780
// videojs.log('eme', 'Received a \'webkitneedkey\' event');
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);
});

// TODO it's possible that the video state must be cleared if reusing the same video
// element between sources
setupSessions(player);
handleWebKitNeedKeyEvent(event, getOptions(player), 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) => {
// TODO convert to videojs.log.debug and add back in
// https://github.com/videojs/video.js/pull/4780
// videojs.log('eme', 'Received a \'webkitneedkey\' event');

// EDGE still fires msneedkey, but should use encrypted instead
if (videojs.browser.IS_EDGE) {
return;
}
// TODO it's possible that the video state must be cleared if reusing the same video
// element between sources
setupSessions(player);
handleWebKitNeedKeyEvent(event, getOptions(player), player.tech_)
.catch(emeError);
});

// IE11 Windows 8.1+
// Since IE11 doesn't support promises, we have to use a combination of
// try/catch blocks and event handling to simulate promise rejection.
// Functionally speaking, there should be no discernible difference between
// the behavior of IE11 and those of other browsers.
player.tech_.el_.addEventListener('msneedkey', (event) => {
// TODO convert to videojs.log.debug and add back in
// https://github.com/videojs/video.js/pull/4780
// videojs.log('eme', 'Received an \'msneedkey\' event');
setupSessions(player);
try {
handleMsNeedKeyEvent(event, getOptions(player), player.eme.sessions, player.tech_);
} catch (error) {
emeError(error);
}
});
player.tech_.on('mskeyerror', emeError);
// TODO: refactor this plugin so it can use a plugin dispose
player.on('dispose', () => {
player.tech_.off('mskeyerror', emeError);
});
} else if (window.MSMediaKeys) {
// IE11 Windows 8.1+
// Since IE11 doesn't support promises, we have to use a combination of
// try/catch blocks and event handling to simulate promise rejection.
// Functionally speaking, there should be no discernible difference between
// the behavior of IE11 and those of other browsers.
player.tech_.el_.addEventListener('msneedkey', (event) => {
// TODO convert to videojs.log.debug and add back in
// https://github.com/videojs/video.js/pull/4780
// videojs.log('eme', 'Received an \'msneedkey\' event');
setupSessions(player);
try {
handleMsNeedKeyEvent(event, getOptions(player), player.eme.sessions, player.tech_);
} catch (error) {
emeError(error);
}
});
player.tech_.on('mskeyerror', emeError);
// TODO: refactor this plugin so it can use a plugin dispose
player.on('dispose', () => {
player.tech_.off('mskeyerror', emeError);
});
}
};

/**
Expand Down
16 changes: 16 additions & 0 deletions test/plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ QUnit.test('initializeMediaKeys ms-prefix', function(assert) {
let errors = 0;
let keySession;
let errorMessage;
const origMediaKeys = window.MediaKeys;
const origWebKitMediaKeys = window.WebKitMediaKeys;

window.MediaKeys = undefined;
window.WebKitMediaKeys = undefined;

if (!window.MSMediaKeys) {
window.MSMediaKeys = () => {};
Expand Down Expand Up @@ -250,6 +255,8 @@ QUnit.test('initializeMediaKeys ms-prefix', function(assert) {
assert.equal(errors, 3, 'error called on player 3 times');
assert.equal(this.player.error(), null,
'no error called on player with suppressError = true');
window.MediaKeys = origMediaKeys;
window.WebKitMediaKeys = origWebKitMediaKeys;
done();
});
this.clock.tick(1);
Expand All @@ -262,7 +269,14 @@ QUnit.test('tech error listener is removed on dispose', function(assert) {
const done = assert.async(1);
let called = 0;
const browser = videojs.browser;
const origMediaKeys = window.MediaKeys;
const origWebKitMediaKeys = window.WebKitMediaKeys;

window.MediaKeys = undefined;
window.WebKitMediaKeys = undefined;
if (!window.MSMediaKeys) {
window.MSMediaKeys = noop.bind(this);
}
// let this test pass on edge
videojs.browser = {IS_EDGE: false};

Expand All @@ -284,6 +298,8 @@ QUnit.test('tech error listener is removed on dispose', function(assert) {

this.player.error = undefined;
videojs.browser = browser;
window.MediaKeys = origMediaKeys;
window.WebKitMediaKeys = origWebKitMediaKeys;
done();
});

Expand Down

0 comments on commit 3a6218f

Please sign in to comment.