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: Add CDM detection module #98

Merged
merged 10 commits into from
Jun 14, 2023
82 changes: 82 additions & 0 deletions src/cdm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import window from 'global/window';
import videojs from 'video.js';

const keySystems = {
fairplay: 'com.apple.fairplay',
playready: 'com.microsoft.playready',
widevine: 'com.widevine.alpha',
clearkey: 'org.w3.clearkey'
};

// Use a combination of API feature and user agent detection to provide an initial
// best guess as to which CDMs are supported.
const bestGuessSupport = {
fairplay: !!window.WebKitMediaKeys,
playready: !!(window.MSMediaKeys && videojs.browser.IE_VERSION) ||
!!(window.MediaKeys && window.navigator.requestMediaKeySystemAccess && videojs.browser.IS_EDGE),
alex-barstow marked this conversation as resolved.
Show resolved Hide resolved
widevine: !!(window.MediaKeys && window.navigator.requestMediaKeySystemAccess) &&
(videojs.browser.IS_CHROME || videojs.browser.IS_FIREFOX),
clearkey: !!(window.MediaKeys && window.navigator.requestMediaKeySystemAccess) &&
(videojs.browser.IS_CHROME || videojs.browser.IS_FIREFOX)
};

let latestSupportResults = bestGuessSupport;

// Synchronously return the latest list of supported CDMs returned by detectCDMSupport().
// If none is available, return the best guess
export const getSupportedCDMs = () => {
return latestSupportResults;
};

// Asynchronously detect the list of supported CDMs by requesting key system access
// when possible, otherwise rely on browser-specific EME API feature detection. This
// is curried to allow passing a promise polyfill from the player options when the
// plugin is initialized. The polyfill is necessary to ensure the function behaves
// consistently between IE (which lacks native promise support) and other browsers
export const createDetectSupportedCDMsFunc = (promise = window.Promise) => () => {
alex-barstow marked this conversation as resolved.
Show resolved Hide resolved
const results = {
fairplay: false,
playready: false,
widevine: false,
clearkey: false
};

if (window.WebKitMediaKeys) {
results.fairplay = true;
}

if (window.MSMediaKeys && window.MSMediaKeys.isTypeSupported(keySystems.playready)) {
results.playready = true;
}

if (window.MediaKeys && window.navigator.requestMediaKeySystemAccess) {
const validConfig = [{
initDataTypes: [],
audioCapabilities: [{
contentType: 'audio/mp4;codecs="mp4a.40.2"'
}],
videoCapabilities: [{
contentType: 'video/mp4;codecs="avc1.42E01E"'
}]
}];

// Currently, Safari doesn't support requestMediaKeySystemAccess() so Fairplay
// is excluded from the checks here
return promise.all([
window.navigator.requestMediaKeySystemAccess(keySystems.widevine, validConfig).catch(() => {}),
window.navigator.requestMediaKeySystemAccess(keySystems.playready, validConfig).catch(() => {}),
window.navigator.requestMediaKeySystemAccess(keySystems.clearkey, validConfig).catch(() => {})
]).then(([widevine, playready, clearkey]) => {
results.widevine = !!widevine;
alex-barstow marked this conversation as resolved.
Show resolved Hide resolved
results.playready = !!playready;
results.clearkey = !!clearkey;
latestSupportResults = results;

return results;
});
}

latestSupportResults = results;

return promise.resolve(results);
};
7 changes: 6 additions & 1 deletion src/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
default as msPrefixed,
PLAYREADY_KEY_SYSTEM
} from './ms-prefixed';
import { getSupportedCDMs, createDetectSupportedCDMsFunc } from './cdm.js';
import { arrayBuffersEqual, arrayBufferFrom } from './utils';

export const hasSession = (sessions, initData) => {
Expand Down Expand Up @@ -294,7 +295,7 @@ const eme = function(options = {}) {
* @param {Object} [emeOptions={}]
* An object of eme plugin options.
* @param {Function} [callback=function(){}]
* @param {Boolean} [suppressErrorIfPossible=false]
* @param {boolean} [suppressErrorIfPossible=false]
*/
initializeMediaKeys(emeOptions = {}, callback = function() {}, suppressErrorIfPossible = false) {
// TODO: this should be refactored and renamed to be less tied
Expand Down Expand Up @@ -351,6 +352,10 @@ const eme = function(options = {}) {
}
}
},
// Pass a promise polyfill from the player options for IE support. If none
alex-barstow marked this conversation as resolved.
Show resolved Hide resolved
// exists, native Promises will be used and the function won't be supported in IE
detectSupportedCDMs: createDetectSupportedCDMsFunc(player.options().Promise),
getSupportedCDMs,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why have a synchronous function for an asynchronous process?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could just get rid of it. Curious whether others think it makes sense to remove getSupportedCDMs()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think getting rid of getSupportedCDMs is the right call here.

options
};
};
Expand Down
74 changes: 74 additions & 0 deletions test/cdm.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import QUnit from 'qunit';
import videojs from 'video.js';
import { getSupportedCDMs, createDetectSupportedCDMsFunc } from '../src/cdm.js';

QUnit.module('videojs-contrib-eme CDM Module');

QUnit.test('detectSupportedCDMs() returns a Promise', function(assert) {
const detectSupportedCDMs = createDetectSupportedCDMsFunc();
const promise = detectSupportedCDMs();

assert.ok(promise.then);
});

QUnit.test('getSupportedCDMs() returns an object with correct properties', function(assert) {
const cdmResults = getSupportedCDMs();
const cdmNames = Object.keys(cdmResults);

assert.equal(cdmNames.length, 4, 'object contains correct number of properties');
assert.equal(cdmNames.includes('fairplay'), true, 'object contains fairplay property');
assert.equal(cdmNames.includes('playready'), true, 'object contains playready property');
assert.equal(cdmNames.includes('widevine'), true, 'object contains widevine property');
assert.equal(cdmNames.includes('clearkey'), true, 'object contains clearkey property');
});

// NOTE: This test is not future-proof. It verifies that the CDM detect function
// works as expected given browser's *current* CDM support. If that support changes,
// this test may need updating.
QUnit.test('detectSupportedCDMs() promise resolves correctly on different browsers', function(assert) {
const done = assert.async();
const detectSupportedCDMs = createDetectSupportedCDMsFunc();
const promise = detectSupportedCDMs();

promise.then((result) => {
if (videojs.browser.IS_FIREFOX) {
assert.deepEqual(result, {
fairplay: false,
playready: false,
widevine: true,
clearkey: true
}, 'widevine and clearkey support reported in Firefox');
}

if (videojs.browser.IS_CHROME) {
// Currently, CDM support should be the same in Chrome and Firefox, but
// Widevine doesn't work in headless Chrome, so for now we just check
// that clearkey: true. When the bug is fixed, this block can be combined
// with the above Firefox block since the behavior should be the same
// https://bugs.chromium.org/p/chromium/issues/detail?id=788662
assert.equal(result.fairplay, false, 'fairplay not supported in Chrome');
assert.equal(result.playready, false, 'playready not supported in Chrome');
assert.equal(result.clearkey, true, 'clearkey is supported in Chrome');
}

if (videojs.browser.IS_ANY_SAFARI) {
assert.deepEqual(result, {
fairplay: true,
playready: false,
widevine: false,
clearkey: false
}, 'fairplay support reported in Safari');
}

if (videojs.browser.IE_VERSION || videojs.browser.IS_EDGE) {
assert.deepEqual(result, {
fairplay: false,
playready: true,
widevine: false,
clearkey: false
}, 'playready support reported in IE/Edge');
}

done();
}).catch(done);
});
10 changes: 10 additions & 0 deletions test/plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ QUnit.test('exposes options', function(assert) {
'exposes publisherId');
});

QUnit.test('exposes getSupportedCDMs() and detectSupportedCDMs()', function(assert) {
assert.notOk(this.player.eme.getSupportedCDMs, 'getSupportedCDMs is unavailable at start');
assert.notOk(this.player.eme.detectSupportedCDMs, 'detectSupportedCDMs is unavailable at start');

this.player.eme();

assert.ok(this.player.eme.getSupportedCDMs, 'getSupportedCDMs is available after initialization');
assert.ok(this.player.eme.detectSupportedCDMs, 'detectSupportedCDMs is available after initialization');
});

// skip test for Safari
if (!window.WebKitMediaKeys) {
QUnit.test('initializeMediaKeys standard', function(assert) {
Expand Down