Skip to content

Commit

Permalink
fix: legacy fairplay (#204)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrums86 committed Feb 17, 2024
1 parent a90edcb commit ee6e512
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 76 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ Maintenance Status: Stable
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->


- [Using](#using)
- [Initialization](#initialization)
- [FairPlay](#fairplay)
Expand Down Expand Up @@ -43,6 +42,7 @@ Maintenance Status: Stable
- [`emeOptions`](#emeoptions)
- [`initializeMediaKeys()`](#initializemediakeys)
- [`detectSupportedCDMs()`](#detectsupportedcdms)
- [`initLegacyFairplay()`](#initlegacyfairplay)
- [Events](#events)
- [`licenserequestattempted`](#licenserequestattempted)
- [`keystatuschange`](#keystatuschange)
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions src/fairplay.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
126 changes: 63 additions & 63 deletions src/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -228,83 +228,33 @@ 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
// try/catch blocks and event handling to simulate promise rejection.
// 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);
}
Expand Down Expand Up @@ -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
};
Expand Down
18 changes: 9 additions & 9 deletions test/fairplay.test.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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');
}
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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');
}
Expand Down Expand Up @@ -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'
};
Expand Down Expand Up @@ -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');
Expand Down

0 comments on commit ee6e512

Please sign in to comment.