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
98 changes: 98 additions & 0 deletions src/cdm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import window from 'global/window';
import videojs from 'video.js';

// `IS_CHROMIUM` and `IS_WINDOWS` are newer Video.js features, so add fallback just in case
export const IS_CHROMIUM = videojs.browser.IS_CHROMIUM || (/Chrome|CriOS/i).test(window.navigator.userAgent);
export const IS_WINDOWS = videojs.browser.IS_WINDOWS || (/Windows/i).test(window.navigator.userAgent);

// Use a combination of API feature and user agent detection to provide an initial
// best guess as to which CDMs are supported.
const hasMediaKeys = Boolean(window.MediaKeys && window.navigator.requestMediaKeySystemAccess);
const isChromeOrFirefox = videojs.browser.IS_CHROME || videojs.browser.IS_FIREFOX;
const isChromiumEdge = videojs.browser.IS_EDGE && IS_CHROMIUM;
const isAnyEdge = videojs.browser.IS_EDGE;

const bestGuessSupport = {
fairplay: Boolean(window.WebKitMediaKeys) || (hasMediaKeys && videojs.browser.IS_ANY_SAFARI),
playready: hasMediaKeys && (isAnyEdge && (!IS_CHROMIUM || IS_WINDOWS)),
widevine: hasMediaKeys && (isChromeOrFirefox || isChromiumEdge),
clearkey: hasMediaKeys && (isChromeOrFirefox || isChromiumEdge)
};

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

const genericConfig = [{
initDataTypes: ['cenc'],
audioCapabilities: [{
contentType: 'audio/mp4;codecs="mp4a.40.2"'
}],
videoCapabilities: [{
contentType: 'video/mp4;codecs="avc1.42E01E"'
}]
}];

const keySystems = [
// Fairplay
// Needs a different config than the others
{
keySystem: 'com.apple.fps',
supportedConfig: [{
initDataTypes: ['sinf'],
videoCapabilities: [{
contentType: 'video/mp4'
}]
}]
},
// Playready
{
keySystem: 'com.microsoft.playready.recommendation',
supportedConfig: genericConfig
},
// Widevine
{
keySystem: 'com.widevine.alpha',
supportedConfig: genericConfig
},
// Clear
{
keySystem: 'org.w3.clearkey',
supportedConfig: genericConfig
}
];

// Asynchronously detect the list of supported CDMs by requesting key system access
// when possible, otherwise rely on browser-specific EME API feature detection.
export const detectSupportedCDMs = () => {
const Promise = window.Promise;
const results = {
fairplay: Boolean(window.WebKitMediaKeys),
playready: false,
widevine: false,
clearkey: false
};

if (!window.MediaKeys || !window.navigator.requestMediaKeySystemAccess) {
latestSupportResults = results;

return Promise.resolve(results);
}

return Promise.all(keySystems.map(({keySystem, supportedConfig}) => {
return window.navigator.requestMediaKeySystemAccess(keySystem, supportedConfig).catch(() => {});
})).then(([fairplay, playready, widevine, clearkey]) => {
results.fairplay = Boolean(fairplay);
results.playready = Boolean(playready);
results.widevine = Boolean(widevine);
results.clearkey = Boolean(clearkey);

latestSupportResults = results;

return results;
});
};
3 changes: 3 additions & 0 deletions 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, detectSupportedCDMs } from './cdm.js';
import { arrayBuffersEqual, arrayBufferFrom, merge } from './utils';
import {version as VERSION} from '../package.json';

Expand Down Expand Up @@ -402,6 +403,8 @@ const eme = function(options = {}) {
}
}
},
detectSupportedCDMs,
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
83 changes: 83 additions & 0 deletions test/cdm.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import QUnit from 'qunit';
import videojs from 'video.js';
import {IS_CHROMIUM, IS_WINDOWS, getSupportedCDMs, detectSupportedCDMs } from '../src/cdm.js';

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

QUnit.test('detectSupportedCDMs() returns a Promise', function(assert) {
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 promise = detectSupportedCDMs();

promise.then((result) => {
// Currently, widevine and clearkey don't work in headless Chrome, so we can't verify cdm support in
// the remote Video.js test environment. However, it can be verified if testing locally in a real browser.
// Headless Chrome bug: https://bugs.chromium.org/p/chromium/issues/detail?id=788662
if (videojs.browser.IS_CHROME) {
assert.equal(result.fairplay, false, 'fairplay not supported in Chrome');
assert.equal(result.playready, false, 'playready not supported in Chrome');

// Uncomment if testing locally in actual browser
// assert.equal(result.clearkey, true, 'clearkey is supported in Chrome');
// assert.equal(result.widevine, true, 'widevine is supported in Chrome');
}

// Widevine requires a plugin in Ubuntu Firefox so it also does not work in the remote Video.js test environment
if (videojs.browser.IS_FIREFOX) {
assert.equal(result.fairplay, false, 'fairplay not supported in FF');
assert.equal(result.playready, false, 'playready not supported in FF');
assert.equal(result.clearkey, true, 'clearkey is supported in FF');

// Uncomment if testing locally in actual browser
// assert.equal(result.widevine, true, 'widevine is supported in Chrome and FF');
}

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

if (videojs.browser.IS_EDGE && IS_CHROMIUM && !IS_WINDOWS) {
assert.deepEqual(result, {
fairplay: false,
playready: false,
widevine: true,
clearkey: true
}, 'widevine support reported in non-Windows Chromium Edge');
}

if (videojs.browser.IS_EDGE && IS_CHROMIUM && IS_WINDOWS) {
assert.deepEqual(result, {
fairplay: false,
playready: true,
widevine: true,
clearkey: true
}, 'widevine and playready support reported in Windows Chromium 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 @@ -107,6 +107,16 @@ QUnit.test('exposes options', function(assert) {
);
});

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 prefix-only Safari
if (!window.MediaKeys) {
QUnit.test('initializeMediaKeys standard', function(assert) {
Expand Down