From ee6e51264c046bea7d9f7c0fe5176e77c5528db9 Mon Sep 17 00:00:00 2001 From: Adam Waldron Date: Sat, 17 Feb 2024 11:17:45 -0800 Subject: [PATCH] fix: legacy fairplay (#204) --- README.md | 9 ++- src/fairplay.js | 6 +- src/plugin.js | 126 +++++++++++++++++++++--------------------- test/fairplay.test.js | 18 +++--- 4 files changed, 83 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 5d38f18..0d990dd 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ Maintenance Status: Stable - - [Using](#using) - [Initialization](#initialization) - [FairPlay](#fairplay) @@ -43,6 +42,7 @@ Maintenance Status: Stable - [`emeOptions`](#emeoptions) - [`initializeMediaKeys()`](#initializemediakeys) - [`detectSupportedCDMs()`](#detectsupportedcdms) + - [`initLegacyFairplay()`](#initlegacyfairplay) - [Events](#events) - [`licenserequestattempted`](#licenserequestattempted) - [`keystatuschange`](#keystatuschange) @@ -573,6 +573,13 @@ player.eme.detectSupportedCDMs() }); ``` +### `initLegacyFairplay()` + +`player.eme.initLegacyFairplay()` is used to init the `'webkitneedskey'` listener when using `WebKitMediaKeys` in Safari. This is useful because Safari currently supports both the modern `com.apple.fps` keysystem through `MediaKeys` and the legacy `com.apple.fps.1_0` keysystem through `WebKitMediaKeys`. Since this plugin will prefer using modern `MediaKeys` over `WebkitMediaKeys` initializing legacy fairplay can be necessary for media using the legacy `1_0` keysystem. + +```js +player.eme.initLegacyFairplay(); +``` _________________________________________________________ ### Events diff --git a/src/fairplay.js b/src/fairplay.js index 179f23a..d2639c5 100644 --- a/src/fairplay.js +++ b/src/fairplay.js @@ -10,7 +10,7 @@ import window from 'global/window'; import {stringToUint16Array, uint16ArrayToString, getHostnameFromUri, mergeAndRemoveNull} from './utils'; import {httpResponseHandler} from './http-handler.js'; -export const FAIRPLAY_KEY_SYSTEM = 'com.apple.fps.1_0'; +export const LEGACY_FAIRPLAY_KEY_SYSTEM = 'com.apple.fps.1_0'; const concatInitDataIdAndCertificate = ({initData, id, cert}) => { if (typeof id === 'string') { @@ -53,7 +53,7 @@ const addKey = ({video, contentId, initData, cert, options, getLicense, eventBus return new Promise((resolve, reject) => { if (!video.webkitKeys) { try { - video.webkitSetMediaKeys(new window.WebKitMediaKeys(FAIRPLAY_KEY_SYSTEM)); + video.webkitSetMediaKeys(new window.WebKitMediaKeys(LEGACY_FAIRPLAY_KEY_SYSTEM)); } catch (error) { reject('Could not create MediaKeys'); return; @@ -153,7 +153,7 @@ export const defaultGetLicense = (fairplayOptions) => { }; const fairplay = ({video, initData, options, eventBus}) => { - const fairplayOptions = options.keySystems[FAIRPLAY_KEY_SYSTEM]; + const fairplayOptions = options.keySystems[LEGACY_FAIRPLAY_KEY_SYSTEM]; const getCertificate = fairplayOptions.getCertificate || defaultGetCertificate(fairplayOptions); const getContentId = fairplayOptions.getContentId || defaultGetContentId; diff --git a/src/plugin.js b/src/plugin.js index d1182fc..07e654a 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -3,7 +3,7 @@ import window from 'global/window'; import { standard5July2016, getSupportedKeySystem } from './eme'; import { default as fairplay, - FAIRPLAY_KEY_SYSTEM + LEGACY_FAIRPLAY_KEY_SYSTEM } from './fairplay'; import { default as msPrefixed, @@ -96,7 +96,7 @@ export const handleEncryptedEvent = (player, event, options, sessions, eventBus) }; export const handleWebKitNeedKeyEvent = (event, options, eventBus) => { - if (!options.keySystems || !options.keySystems[FAIRPLAY_KEY_SYSTEM] || !event.initData) { + if (!options.keySystems || !options.keySystems[LEGACY_FAIRPLAY_KEY_SYSTEM] || !event.initData) { // return silently since it may be handled by a different system return Promise.resolve(); } @@ -228,70 +228,22 @@ const onPlayerReady = (player, emeError) => { setupSessions(player); - if (window.MediaKeys) { + const playerOptions = getOptions(player); + // Legacy fairplay is the keysystem 'com.apple.fps.1_0'. + // If we are using this keysystem we want to use WebkitMediaKeys. + const isLegacyFairplay = playerOptions.keySystem && playerOptions.keySystem[LEGACY_FAIRPLAY_KEY_SYSTEM]; + + if (window.MediaKeys && !isLegacyFairplay) { // Support EME 05 July 2016 // Chrome 42+, Firefox 47+, Edge, Safari 12.1+ on macOS 10.14+ player.tech_.el_.addEventListener('encrypted', (event) => { - // TODO convert to videojs.log.debug and add back in - // https://github.com/videojs/video.js/pull/4780 - // videojs.log('eme', 'Received an \'encrypted\' event'); + videojs.log.debug('eme', 'Received an \'encrypted\' event'); setupSessions(player); - handleEncryptedEvent(player, event, getOptions(player), player.eme.sessions, player.tech_) + handleEncryptedEvent(player, event, playerOptions, player.eme.sessions, player.tech_) .catch(emeError); }); } else if (window.WebKitMediaKeys) { - const handleFn = (event) => { - // TODO convert to videojs.log.debug and add back in - // https://github.com/videojs/video.js/pull/4780 - // videojs.log('eme', 'Received a \'webkitneedkey\' event'); - - // TODO it's possible that the video state must be cleared if reusing the same video - // element between sources - setupSessions(player); - handleWebKitNeedKeyEvent(event, getOptions(player), player.tech_) - .catch(emeError); - }; - - // Support Safari EME with FairPlay - // (also used in early Chrome or Chrome with EME disabled flag) - player.tech_.el_.addEventListener('webkitneedkey', (event) => { - const options = getOptions(player); - const firstWebkitneedkeyTimeout = options.firstWebkitneedkeyTimeout || 1000; - const src = player.src(); - // on source change or first startup reset webkitneedkey options. - - player.eme.webkitneedkey_ = player.eme.webkitneedkey_ || {}; - - // if the source changed we need to handle the first event again. - // track source changes internally. - if (player.eme.webkitneedkey_.src !== src) { - player.eme.webkitneedkey_ = { - handledFirstEvent: false, - src - }; - } - // It's possible that at the start of playback a rendition switch - // on a small player in safari's HLS implementation will cause - // two webkitneedkey events to occur. We want to make sure to cancel - // our first existing request if we get another within 1 second. This - // prevents a non-fatal player error from showing up due to a - // request failure. - if (!player.eme.webkitneedkey_.handledFirstEvent) { - // clear the old timeout so that a new one can be created - // with the new rendition's event data - player.clearTimeout(player.eme.webkitneedkey_.timeout); - player.eme.webkitneedkey_.timeout = player.setTimeout(() => { - player.eme.webkitneedkey_.handledFirstEvent = true; - player.eme.webkitneedkey_.timeout = null; - handleFn(event); - }, firstWebkitneedkeyTimeout); - // after we have a verified first request, we will request on - // every other event like normal. - } else { - handleFn(event); - } - }); - + player.eme.initLegacyFairplay(); } else if (window.MSMediaKeys) { // IE11 Windows 8.1+ // Since IE11 doesn't support promises, we have to use a combination of @@ -299,12 +251,10 @@ const onPlayerReady = (player, emeError) => { // Functionally speaking, there should be no discernible difference between // the behavior of IE11 and those of other browsers. player.tech_.el_.addEventListener('msneedkey', (event) => { - // TODO convert to videojs.log.debug and add back in - // https://github.com/videojs/video.js/pull/4780 - // videojs.log('eme', 'Received an \'msneedkey\' event'); + videojs.log.debug('eme', 'Received an \'msneedkey\' event'); setupSessions(player); try { - handleMsNeedKeyEvent(event, getOptions(player), player.eme.sessions, player.tech_); + handleMsNeedKeyEvent(event, playerOptions, player.eme.sessions, player.tech_); } catch (error) { emeError(error); } @@ -403,6 +353,56 @@ const eme = function(options = {}) { } } }, + initLegacyFairplay() { + const playerOptions = getOptions(player); + const handleFn = (event) => { + videojs.log.debug('eme', 'Received a \'webkitneedkey\' event'); + // TODO it's possible that the video state must be cleared if reusing the same video + // element between sources + setupSessions(player); + handleWebKitNeedKeyEvent(event, playerOptions, player.tech_) + .catch(emeError); + }; + + // Support Safari EME with FairPlay + // (also used in early Chrome or Chrome with EME disabled flag) + player.tech_.el_.addEventListener('webkitneedkey', (event) => { + const firstWebkitneedkeyTimeout = playerOptions.firstWebkitneedkeyTimeout || 1000; + const src = player.src(); + // on source change or first startup reset webkitneedkey options. + + player.eme.webkitneedkey_ = player.eme.webkitneedkey_ || {}; + + // if the source changed we need to handle the first event again. + // track source changes internally. + if (player.eme.webkitneedkey_.src !== src) { + player.eme.webkitneedkey_ = { + handledFirstEvent: false, + src + }; + } + // It's possible that at the start of playback a rendition switch + // on a small player in safari's HLS implementation will cause + // two webkitneedkey events to occur. We want to make sure to cancel + // our first existing request if we get another within 1 second. This + // prevents a non-fatal player error from showing up due to a + // request failure. + if (!player.eme.webkitneedkey_.handledFirstEvent) { + // clear the old timeout so that a new one can be created + // with the new rendition's event data + player.clearTimeout(player.eme.webkitneedkey_.timeout); + player.eme.webkitneedkey_.timeout = player.setTimeout(() => { + player.eme.webkitneedkey_.handledFirstEvent = true; + player.eme.webkitneedkey_.timeout = null; + handleFn(event); + }, firstWebkitneedkeyTimeout); + // after we have a verified first request, we will request on + // every other event like normal. + } else { + handleFn(event); + } + }); + }, detectSupportedCDMs, options }; diff --git a/test/fairplay.test.js b/test/fairplay.test.js index 3b3d1a5..4e77d12 100644 --- a/test/fairplay.test.js +++ b/test/fairplay.test.js @@ -1,9 +1,9 @@ import QUnit from 'qunit'; import { default as fairplay, - FAIRPLAY_KEY_SYSTEM, defaultGetLicense, - defaultGetCertificate + defaultGetCertificate, + LEGACY_FAIRPLAY_KEY_SYSTEM } from '../src/fairplay'; import videojs from 'video.js'; import window from 'global/window'; @@ -170,7 +170,7 @@ QUnit.test('error in getCertificate rejects promise', function(assert) { const keySystems = {}; const done = assert.async(1); - keySystems[FAIRPLAY_KEY_SYSTEM] = { + keySystems[LEGACY_FAIRPLAY_KEY_SYSTEM] = { getCertificate: (options, callback) => { callback('error in getCertificate'); } @@ -195,7 +195,7 @@ QUnit.test('error in WebKitMediaKeys rejects promise', function(assert) { throw new Error('unsupported keySystem'); }; - keySystems[FAIRPLAY_KEY_SYSTEM] = {}; + keySystems[LEGACY_FAIRPLAY_KEY_SYSTEM] = {}; fairplay({ video, @@ -221,7 +221,7 @@ QUnit.test('error in webkitSetMediaKeys rejects promise', function(assert) { window.WebKitMediaKeys = function() {}; - keySystems[FAIRPLAY_KEY_SYSTEM] = {}; + keySystems[LEGACY_FAIRPLAY_KEY_SYSTEM] = {}; fairplay({ video, @@ -251,7 +251,7 @@ QUnit.test('error in webkitKeys.createSession rejects promise', function(assert) window.WebKitMediaKeys = function() {}; - keySystems[FAIRPLAY_KEY_SYSTEM] = {}; + keySystems[LEGACY_FAIRPLAY_KEY_SYSTEM] = {}; fairplay({ video, @@ -290,7 +290,7 @@ QUnit.test('error in getLicense rejects promise', function(assert) { window.WebKitMediaKeys = function() {}; - keySystems[FAIRPLAY_KEY_SYSTEM] = { + keySystems[LEGACY_FAIRPLAY_KEY_SYSTEM] = { getLicense: (options, contentId, message, callback) => { callback('error in getLicense'); } @@ -336,7 +336,7 @@ QUnit.test('keysessioncreated fired on key session created', function(assert) { window.WebKitMediaKeys = function() {}; - keySystems[FAIRPLAY_KEY_SYSTEM] = { + keySystems[LEGACY_FAIRPLAY_KEY_SYSTEM] = { licenseUri: 'some-url', certificateUri: 'some-other-url' }; @@ -376,7 +376,7 @@ QUnit.test('a webkitkeyerror rejects promise', function(assert) { window.WebKitMediaKeys = function() {}; - keySystems[FAIRPLAY_KEY_SYSTEM] = { + keySystems[LEGACY_FAIRPLAY_KEY_SYSTEM] = { getLicense: (options, contentId, message, callback) => { callback(null); keySession.trigger('webkitkeyerror');