Skip to content

Commit

Permalink
feat: Trigger player errors from EME errors and refactor to use promi…
Browse files Browse the repository at this point in the history
…ses internally.
  • Loading branch information
squarebracket authored and misteroneill committed Mar 11, 2019
1 parent dc5d8c4 commit 7cae936
Show file tree
Hide file tree
Showing 9 changed files with 1,069 additions and 309 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,9 @@ player.src({
### initializeMediaKeys
Type: `function`

`player.eme.initializeMediaKeys()` sets up MediaKeys immediately on demand. This is useful for setting up the video element for DRM before loading any content. Otherwise the video element is set up for DRM on `encrypted` events. This is not supported in Safari.
`player.eme.initializeMediaKeys()` sets up MediaKeys immediately on demand. This is useful
for setting up the video element for DRM before loading any content. Otherwise the video
element is set up for DRM on `encrypted` events. This is not supported in Safari.

```javascript
// additional plugin options
Expand All @@ -342,15 +344,23 @@ var emeOptions = {
}
};

player.eme.initializeMediaKeys(emeOptions, function(error) {
var emeCallback = function(error) {
if (error) {
// do something with error
}

// do something else
});
};

player.eme.initializeMediaKeys(emeOptions, emeCallback, suppressErrorsIfPossible);
```

When `suppressErrorsIfPossible` is set to `false` (the default) and an error
occurs, the error handler will be invoked after the callback finishes, and
`error` called on the player. When set to `true` and an error occurs, the
error handler will not be invoked, with the exception of `mskeyerror` errors
in IE11 since they cannot be suppressed asynchronously.

### Passing methods seems complicated

While simple URLs are supported for many EME implementations, we wanted to provide as much
Expand Down
185 changes: 95 additions & 90 deletions src/eme.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,62 +47,66 @@ export const makeNewRequest = ({
}) => {
const keySession = mediaKeys.createSession();

keySession.addEventListener('message', (event) => {
getLicense(options, event.message)
.then((license) => {
return keySession.update(license);
})
.catch(videojs.log.error.bind(videojs.log.error, 'failed to get and set license'));
}, false);

keySession.addEventListener('keystatuseschange', (event) => {
let expired = false;

// based on https://www.w3.org/TR/encrypted-media/#example-using-all-events
keySession.keyStatuses.forEach((status, keyId) => {
// Trigger an event so that outside listeners can take action if appropriate.
// For instance, the `output-restricted` status should result in an
// error being thrown.
eventBus.trigger({
keyId,
status,
target: keySession,
type: 'keystatuschange'
return new Promise((resolve, reject) => {

keySession.addEventListener('message', (event) => {
getLicense(options, event.message)
.then((license) => {
resolve(keySession.update(license));
})
.catch((err) => {
reject(err);
});
}, false);

keySession.addEventListener('keystatuseschange', (event) => {
let expired = false;

// based on https://www.w3.org/TR/encrypted-media/#example-using-all-events
keySession.keyStatuses.forEach((status, keyId) => {
// Trigger an event so that outside listeners can take action if appropriate.
// For instance, the `output-restricted` status should result in an
// error being thrown.
eventBus.trigger({
keyId,
status,
target: keySession,
type: 'keystatuschange'
});
switch (status) {
case 'expired':
// If one key is expired in a session, all keys are expired. From
// https://www.w3.org/TR/encrypted-media/#dom-mediakeystatus-expired, "All other
// keys in the session must have this status."
expired = true;
break;
case 'internal-error':
// "This value is not actionable by the application."
// https://www.w3.org/TR/encrypted-media/#dom-mediakeystatus-internal-error
videojs.log.warn(
'Key status reported as "internal-error." Leaving the session open since we ' +
'don\'t have enough details to know if this error is fatal.', event);
break;
}
});
switch (status) {
case 'expired':
// If one key is expired in a session, all keys are expired. From
// https://www.w3.org/TR/encrypted-media/#dom-mediakeystatus-expired, "All other
// keys in the session must have this status."
expired = true;
break;
case 'internal-error':
// "This value is not actionable by the application."
// https://www.w3.org/TR/encrypted-media/#dom-mediakeystatus-internal-error
videojs.log.warn(
'Key status reported as "internal-error." Leaving the session open since we ' +
'don\'t have enough details to know if this error is fatal.', event);
break;
}
});

if (expired) {
// Close session and remove it from the session list to ensure that a new
// session can be created.
//
// TODO convert to videojs.log.debug and add back in
// https://github.com/videojs/video.js/pull/4780
// videojs.log.debug('Session expired, closing the session.');
keySession.close().then(() => {
removeSession(initData);
});
}
}, false);
if (expired) {
// Close session and remove it from the session list to ensure that a new
// session can be created.
//
// TODO convert to videojs.log.debug and add back in
// https://github.com/videojs/video.js/pull/4780
// videojs.log.debug('Session expired, closing the session.');
keySession.close().then(() => {
removeSession(initData);
});
}
}, false);

keySession.generateRequest(initDataType, initData).catch(
videojs.log.error.bind(videojs.log.error,
'Unable to create or initialize key session')
);
keySession.generateRequest(initDataType, initData).catch(() => {
reject('Unable to create or initialize key session');
});
});
};

const addSession = ({
Expand All @@ -115,7 +119,7 @@ const addSession = ({
eventBus
}) => {
if (video.mediaKeysObject) {
makeNewRequest({
return makeNewRequest({
mediaKeys: video.mediaKeysObject,
initDataType,
initData,
Expand All @@ -124,9 +128,10 @@ const addSession = ({
removeSession,
eventBus
});
} else {
video.pendingSessionData.push({initDataType, initData});
}

video.pendingSessionData.push({initDataType, initData});
return Promise.resolve();
};

const setMediaKeys = ({
Expand All @@ -139,28 +144,31 @@ const setMediaKeys = ({
eventBus
}) => {
video.mediaKeysObject = createdMediaKeys;
const promises = [];

if (certificate) {
createdMediaKeys.setServerCertificate(certificate);
promises.push(createdMediaKeys.setServerCertificate(certificate));
}

for (let i = 0; i < video.pendingSessionData.length; i++) {
const data = video.pendingSessionData[i];

makeNewRequest({
promises.push(makeNewRequest({
mediaKeys: video.mediaKeysObject,
initDataType: data.initDataType,
initData: data.initData,
options,
getLicense,
removeSession,
eventBus
});
}));
}

video.pendingSessionData = [];

return video.setMediaKeys(createdMediaKeys);
promises.push(video.setMediaKeys(createdMediaKeys));

return Promise.all(promises);
};

const defaultPlayreadyGetLicense = (url) => (emeOptions, keyMessage, callback) => {
Expand Down Expand Up @@ -202,6 +210,7 @@ const promisifyGetLicense = (getLicenseFn, eventBus) => {
}
if (err) {
reject(err);
return;
}

resolve(license);
Expand Down Expand Up @@ -232,6 +241,7 @@ export const standard5July2016 = ({
video,
initDataType,
initData,
keySystemAccess,
options,
removeSession,
eventBus
Expand All @@ -248,39 +258,30 @@ export const standard5July2016 = ({
let certificate;
let keySystemOptions;

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

if (!keySystemPromise) {
videojs.log.error('No supported key system found');
return Promise.resolve();
}

keySystemPromise = keySystemPromise.then((keySystemAccess) => {
return new Promise((resolve, reject) => {
// save key system for adding sessions
video.keySystem = keySystemAccess.keySystem;
keySystemOptions = standardizeKeySystemOptions(
keySystemAccess.keySystem,
options.keySystems[keySystemAccess.keySystem]);

keySystemOptions = standardizeKeySystemOptions(
keySystemAccess.keySystem,
options.keySystems[keySystemAccess.keySystem]);
if (!keySystemOptions.getCertificate) {
resolve(keySystemAccess);
return;
}

if (!keySystemOptions.getCertificate) {
resolve(keySystemAccess);
keySystemOptions.getCertificate(options, (err, cert) => {
if (err) {
reject(err);
return;
}

keySystemOptions.getCertificate(options, (err, cert) => {
if (err) {
reject(err);
return;
}

certificate = cert;
certificate = cert;

resolve(keySystemAccess);
});
resolve();
});
}).then((keySystemAccess) => {
}).then(() => {
return keySystemAccess.createMediaKeys();
}).then((createdMediaKeys) => {
return setMediaKeys({
Expand All @@ -292,14 +293,18 @@ export const standard5July2016 = ({
removeSession,
eventBus
});
}).catch(
videojs.log.error.bind(videojs.log.error,
'Failed to create and initialize a MediaKeys object')
);
}).catch((err) => {
// if we have a specific error message, use it, otherwise show a more
// generic one
if (err) {
return Promise.reject(err);
}
return Promise.reject('Failed to create and initialize a MediaKeys object');
});
}

return keySystemPromise.then(() => {
addSession({
return addSession({
video,
initDataType,
initData,
Expand Down
40 changes: 25 additions & 15 deletions src/fairplay.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/**
* The W3C Working Draft of 22 October 2013 seems to be the best match for
* the ms-prefixed API. However, it should only be used as a guide; it is
* doubtful the spec is 100% implemented as described.
* @see https://www.w3.org/TR/2013/WD-encrypted-media-20131022
*/
import videojs from 'video.js';
import window from 'global/window';
import {stringToUint16Array, uint8ArrayToString, getHostnameFromUri} from './utils';
Expand Down Expand Up @@ -45,19 +51,21 @@ const concatInitDataIdAndCertificate = ({initData, id, cert}) => {
const addKey = ({video, contentId, initData, cert, options, getLicense, eventBus}) => {
return new Promise((resolve, reject) => {
if (!video.webkitKeys) {
video.webkitSetMediaKeys(new window.WebKitMediaKeys(FAIRPLAY_KEY_SYSTEM));
}

if (!video.webkitKeys) {
reject('Could not create MediaKeys');
return;
try {
video.webkitSetMediaKeys(new window.WebKitMediaKeys(FAIRPLAY_KEY_SYSTEM));
} catch (error) {
reject('Could not create MediaKeys');
return;
}
}

const keySession = video.webkitKeys.createSession(
'video/mp4',
concatInitDataIdAndCertificate({id: contentId, initData, cert}));
let keySession;

if (!keySession) {
try {
keySession = video.webkitKeys.createSession(
'video/mp4',
concatInitDataIdAndCertificate({id: contentId, initData, cert}));
} catch (error) {
reject('Could not create key session');
return;
}
Expand All @@ -78,13 +86,15 @@ const addKey = ({video, contentId, initData, cert, options, getLicense, eventBus
});
});

keySession.addEventListener('webkitkeyadded', (event) => {
resolve(event);
keySession.addEventListener('webkitkeyadded', () => {
resolve();
});

// for testing purposes, adding webkitkeyerror must be the last item in this method
keySession.addEventListener('webkitkeyerror', (event) => {
reject(event);
keySession.addEventListener('webkitkeyerror', () => {
const error = keySession.error;

reject(`KeySession error: code ${error.code}, systemCode ${error.systemCode}`);
});
});
};
Expand Down Expand Up @@ -157,7 +167,7 @@ const fairplay = ({video, initData, options, eventBus}) => {
contentId: getContentId(options, initData),
eventBus
});
}).catch(videojs.log.error.bind(videojs.log.error));
});
};

export default fairplay;
Loading

0 comments on commit 7cae936

Please sign in to comment.