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

feat: eme error interface #208

Merged
merged 27 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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
343 changes: 232 additions & 111 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@
"test/"
],
"dependencies": {
"global": "^4.3.2",
"video.js": "^6 || ^7 || ^8"
"global": "^4.3.2"
},
"peerDependencies": {
"video.js": "^8.11.8"
},
dzianis-dashkevich marked this conversation as resolved.
Show resolved Hide resolved
"devDependencies": {
"conventional-changelog-cli": "^2.0.12",
Expand Down
223 changes: 149 additions & 74 deletions src/eme.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,84 +101,129 @@ export const makeNewRequest = (player, requestOptions) => {
getLicense,
removeSession,
eventBus,
contentId
contentId,
emeError,
keySystem
} = requestOptions;

const keySession = mediaKeys.createSession();

eventBus.trigger('keysessioncreated');
try {
const keySession = mediaKeys.createSession();

player.on('dispose', () => {
keySession.close();
});
eventBus.trigger('keysessioncreated');

return new Promise((resolve, reject) => {
keySession.addEventListener('message', (event) => {
// all other types will be handled by keystatuseschange
if (event.messageType !== 'license-request' && event.messageType !== 'license-renewal') {
return;
}

getLicense(options, event.message, contentId)
.then((license) => {
resolve(keySession.update(license));
})
.catch((err) => {
reject(err);
});
}, false);
player.on('dispose', () => {
keySession.close().then(() => {
eventBus.trigger('keysessionclosed');
}).catch((error) => {
const metadata = {
errorType: videojs.Error.EMEFailedToCloseSession,
keySystem
};

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

// 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.
return new Promise((resolve, reject) => {
keySession.addEventListener('message', (event) => {
eventBus.trigger({
keyId,
status,
target: keySession,
type: 'keystatuschange'
type: 'keymessage',
event
});
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':
const message =
'Key status reported as "internal-error." Leaving the session open since we ' +
'don\'t have enough details to know if this error is fatal.';

// "This value is not actionable by the application."
// https://www.w3.org/TR/encrypted-media/#dom-mediakeystatus-internal-error
videojs.log.warn(message, event);
break;
// all other types will be handled by keystatuseschange
if (event.messageType !== 'license-request' && event.messageType !== 'license-renewal') {
return;
}
});

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);
makeNewRequest(player, requestOptions);
getLicense(options, event.message, contentId)
.then((license) => {
resolve(keySession.update(license).then(() => {
eventBus.trigger('keysessionupdated');
}).catch((error) => {
const metadata = {
errorType: videojs.Error.EMEFailedToUpdateSessionWithReceivedLicenseKeys,
keySystem
};

emeError(error, metadata);
}));
})
.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':
const message =
'Key status reported as "internal-error." Leaving the session open since we ' +
'don\'t have enough details to know if this error is fatal.';

// "This value is not actionable by the application."
// https://www.w3.org/TR/encrypted-media/#dom-mediakeystatus-internal-error
videojs.log.warn(message, event);
break;
}
});
}
}, false);

keySession.generateRequest(initDataType, initData).catch(() => {
reject('Unable to create or initialize key session');
if (expired) {
// Close session and remove it from the session list to ensure that a new
// session can be created.
videojs.log.debug('Session expired, closing the session.');
keySession.close().then(() => {
eventBus.trigger('keysessionclosed');
removeSession(initData);
makeNewRequest(player, requestOptions);
}).catch((error) => {
const metadata = {
errorType: videojs.Error.EMEFailedToCloseSession,
keySystem
};

emeError(error, metadata);
});
}
}, false);

keySession.generateRequest(initDataType, initData).catch((error) => {
const metadata = {
errorType: videojs.Error.EMEFailedToGenerateLicenseRequest,
keySystem
};

emeError(error, metadata);
reject('Unable to create or initialize key session');
});
});
});

} catch (error) {
const metadata = {
errorType: videojs.Error.EMEFailedToCreateMediaKeySession,
keySystem
};

emeError(error, metadata);
}
};

/*
Expand Down Expand Up @@ -217,7 +262,8 @@ export const addSession = ({
getLicense,
contentId,
removeSession,
eventBus
eventBus,
emeError
}) => {
const sessionData = {
initDataType,
Expand All @@ -226,7 +272,9 @@ export const addSession = ({
getLicense,
removeSession,
eventBus,
contentId
contentId,
emeError,
keySystem: video.keySystem
};

if (video.mediaKeysObject) {
Expand Down Expand Up @@ -262,15 +310,23 @@ export const addPendingSessions = ({
player,
video,
certificate,
createdMediaKeys
createdMediaKeys,
emeError
}) => {
// save media keys on the video element to act as a reference for other functions so
// that they don't recreate the keys
video.mediaKeysObject = createdMediaKeys;
const promises = [];

if (certificate) {
promises.push(createdMediaKeys.setServerCertificate(certificate));
promises.push(createdMediaKeys.setServerCertificate(certificate).catch((error) => {
const metadata = {
errorType: videojs.Error.EMEFailedToSetServerCertificate,
keySystem: video.keySystem
};

emeError(error, metadata);
}));
}

for (let i = 0; i < video.pendingSessionData.length; i++) {
Expand All @@ -284,13 +340,22 @@ export const addPendingSessions = ({
getLicense: data.getLicense,
removeSession: data.removeSession,
eventBus: data.eventBus,
contentId: data.contentId
contentId: data.contentId,
emeError: data.emeError,
keySystem: video.keySystem
}));
}

video.pendingSessionData = [];

promises.push(video.setMediaKeys(createdMediaKeys));
promises.push(video.setMediaKeys(createdMediaKeys).catch((error) => {
const metadata = {
errorType: videojs.Error.EMEFailedToAttachMediaKeysToVideoElement,
keySystem: video.keySystem
};

emeError(error, metadata);
}));

return Promise.all(promises);
};
Expand Down Expand Up @@ -388,7 +453,8 @@ export const standard5July2016 = ({
keySystemAccess,
options,
removeSession,
eventBus
eventBus,
emeError
}) => {
let keySystemPromise = Promise.resolve();
const keySystem = keySystemAccess.keySystem;
Expand Down Expand Up @@ -438,13 +504,21 @@ export const standard5July2016 = ({
}).then(() => {
return keySystemAccess.createMediaKeys();
}).then((createdMediaKeys) => {
eventBus.trigger('keysystemaccesscomplete');
return addPendingSessions({
player,
video,
certificate,
createdMediaKeys
createdMediaKeys,
emeError
});
}).catch((err) => {
const metadata = {
errorType: videojs.Error.EMEFailedToCreateMediaKeys,
keySystem
};

emeError(err, metadata);
// if we have a specific error message, use it, otherwise show a more
// generic one
if (err) {
Expand All @@ -468,7 +542,8 @@ export const standard5July2016 = ({
getLicense,
contentId,
removeSession,
eventBus
eventBus,
emeError
});
});
};
Loading
Loading