diff --git a/README.md b/README.md index 6f567c3..5d38f18 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Maintenance Status: Stable - [Header Hierarchy and Removal](#header-hierarchy-and-removal) - [`emeOptions`](#emeoptions) - [`initializeMediaKeys()`](#initializemediakeys) + - [`detectSupportedCDMs()`](#detectsupportedcdms) - [Events](#events) - [`licenserequestattempted`](#licenserequestattempted) - [`keystatuschange`](#keystatuschange) @@ -558,6 +559,22 @@ 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()` will be 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. +### `detectSupportedCDMs()` + +`player.eme.detectSupportedCDMs()` is used to asynchronously detect and return a list of supported Content Decryption Modules (CDMs) in the current browser. It uses the EME API to request access to each key system and determine its availability. This function checks for the support of the following key systems: FairPlay, PlayReady, Widevine, and ClearKey. + +Please use this function sparingly, as side-effects (namely calling `navigator.requestMediaKeySystemAccess()`) can have user-visible effects, such as prompting for system resource permissions, which could be disruptive if invoked at inappropriate times. See [requestMediaKeySystemAccess()](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/requestMediaKeySystemAccess) documentation for more information. + +```js +player.eme.detectSupportedCDMs() + .then(supportedCDMs => { + // Sample output: {fairplay: false, playready: false, widevine: true, clearkey: true} + console.log(supportedCDMs); + }); +``` + +_________________________________________________________ + ### Events There are some events that are specific to this plugin. diff --git a/src/cdm.js b/src/cdm.js new file mode 100644 index 0000000..5abf0dd --- /dev/null +++ b/src/cdm.js @@ -0,0 +1,67 @@ +import window from 'global/window'; + +const genericConfig = [{ + initDataTypes: ['cenc'], + audioCapabilities: [{ + contentType: 'audio/mp4;codecs="mp4a.40.2"' + }], + videoCapabilities: [{ + contentType: 'video/mp4;codecs="avc1.42E01E"' + }] +}]; + +const keySystems = [ + // Fairplay + // Requires different config than other CDMs + { + 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) { + 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); + + return results; + }); +}; diff --git a/src/plugin.js b/src/plugin.js index 53cb8e9..d1182fc 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -9,6 +9,7 @@ import { default as msPrefixed, PLAYREADY_KEY_SYSTEM } from './ms-prefixed'; +import {detectSupportedCDMs } from './cdm.js'; import { arrayBuffersEqual, arrayBufferFrom, merge } from './utils'; import {version as VERSION} from '../package.json'; @@ -402,6 +403,7 @@ const eme = function(options = {}) { } } }, + detectSupportedCDMs, options }; }; diff --git a/test/cdm.test.js b/test/cdm.test.js new file mode 100644 index 0000000..b94afd9 --- /dev/null +++ b/test/cdm.test.js @@ -0,0 +1,77 @@ +import QUnit from 'qunit'; +import window from 'global/window'; +import videojs from 'video.js'; +import {detectSupportedCDMs } from '../src/cdm.js'; + +// `IS_CHROMIUM` and `IS_WINDOWS` are newer Video.js features, so add fallback just in case +const IS_CHROMIUM = videojs.browser.IS_CHROMIUM || (/Chrome|CriOS/i).test(window.navigator.userAgent); +const IS_WINDOWS = videojs.browser.IS_WINDOWS || (/Windows/i).test(window.navigator.userAgent); + +QUnit.module('videojs-contrib-eme CDM Module'); + +QUnit.test('detectSupportedCDMs() returns a Promise', function(assert) { + const promise = detectSupportedCDMs(); + + assert.ok(promise.then); +}); + +// 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); +}); diff --git a/test/plugin.test.js b/test/plugin.test.js index 613b883..838c88c 100644 --- a/test/plugin.test.js +++ b/test/plugin.test.js @@ -107,6 +107,14 @@ QUnit.test('exposes options', function(assert) { ); }); +QUnit.test('exposes detectSupportedCDMs()', function(assert) { + assert.notOk(this.player.eme.detectSupportedCDMs, 'detectSupportedCDMs is unavailable at start'); + + this.player.eme(); + + 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) {