-
Notifications
You must be signed in to change notification settings - Fork 70
/
plugin.js
415 lines (368 loc) · 13.8 KB
/
plugin.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
import videojs from 'video.js';
import window from 'global/window';
import { standard5July2016, getSupportedKeySystem } from './eme';
import {
default as fairplay,
FAIRPLAY_KEY_SYSTEM
} from './fairplay';
import {
default as msPrefixed,
PLAYREADY_KEY_SYSTEM
} from './ms-prefixed';
import { arrayBuffersEqual, arrayBufferFrom, merge } from './utils';
import {version as VERSION} from '../package.json';
export const hasSession = (sessions, initData) => {
for (let i = 0; i < sessions.length; i++) {
// Other types of sessions may be in the sessions array that don't store the initData
// (for instance, PlayReady sessions on IE11).
if (!sessions[i].initData) {
continue;
}
// initData should be an ArrayBuffer by the spec:
// eslint-disable-next-line max-len
// @see [Media Encrypted Event initData Spec]{@link https://www.w3.org/TR/encrypted-media/#mediaencryptedeventinit}
//
// However, on some browsers it may come back with a typed array view of the buffer.
// This is the case for IE11, however, since IE11 sessions are handled differently
// (following the msneedkey PlayReady path), this coversion may not be important. It
// is safe though, and might be a good idea to retain in the short term (until we have
// catalogued the full range of browsers and their implementations).
const sessionBuffer = arrayBufferFrom(sessions[i].initData);
const initDataBuffer = arrayBufferFrom(initData);
if (arrayBuffersEqual(sessionBuffer, initDataBuffer)) {
return true;
}
}
return false;
};
export const removeSession = (sessions, initData) => {
for (let i = 0; i < sessions.length; i++) {
if (sessions[i].initData === initData) {
sessions.splice(i, 1);
return;
}
}
};
export const handleEncryptedEvent = (player, event, options, sessions, eventBus) => {
if (!options || !options.keySystems) {
// return silently since it may be handled by a different system
return Promise.resolve();
}
let initData = event.initData;
return getSupportedKeySystem(options.keySystems).then((keySystemAccess) => {
const keySystem = keySystemAccess.keySystem;
// Use existing init data from options if provided
if (options.keySystems[keySystem] &&
options.keySystems[keySystem].pssh) {
initData = options.keySystems[keySystem].pssh;
}
// "Initialization Data must be a fixed value for a given set of stream(s) or media
// data. It must only contain information related to the keys required to play a given
// set of stream(s) or media data."
// eslint-disable-next-line max-len
// @see [Initialization Data Spec]{@link https://www.w3.org/TR/encrypted-media/#initialization-data}
if (hasSession(sessions, initData) || !initData) {
// TODO convert to videojs.log.debug and add back in
// https://github.com/videojs/video.js/pull/4780
// videojs.log('eme',
// 'Already have a configured session for init data, ignoring event.');
return Promise.resolve();
}
sessions.push({ initData });
return standard5July2016({
player,
video: event.target,
initDataType: event.initDataType,
initData,
keySystemAccess,
options,
removeSession: removeSession.bind(null, sessions),
eventBus
});
});
};
export const handleWebKitNeedKeyEvent = (event, options, eventBus) => {
if (!options.keySystems || !options.keySystems[FAIRPLAY_KEY_SYSTEM] || !event.initData) {
// return silently since it may be handled by a different system
return Promise.resolve();
}
// From Apple's example Safari FairPlay integration code, webkitneedkey is not repeated
// for the same content. Unless documentation is found to present the opposite, handle
// all webkitneedkey events the same (even if they are repeated).
return fairplay({
video: event.target,
initData: event.initData,
options,
eventBus
});
};
export const handleMsNeedKeyEvent = (event, options, sessions, eventBus) => {
if (!options.keySystems || !options.keySystems[PLAYREADY_KEY_SYSTEM]) {
// return silently since it may be handled by a different system
return;
}
// "With PlayReady content protection, your Web app must handle the first needKey event,
// but it must then ignore any other needKey event that occurs."
// eslint-disable-next-line max-len
// @see [PlayReady License Acquisition]{@link https://msdn.microsoft.com/en-us/library/dn468979.aspx}
//
// Usually (and as per the example in the link above) this is determined by checking for
// the existence of video.msKeys. However, since the video element may be reused, it's
// easier to directly manage the session.
if (sessions.reduce((acc, session) => acc || session.playready, false)) {
// TODO convert to videojs.log.debug and add back in
// https://github.com/videojs/video.js/pull/4780
// videojs.log('eme',
// 'An \'msneedkey\' event was receieved earlier, ignoring event.');
return;
}
let initData = event.initData;
// Use existing init data from options if provided
if (options.keySystems[PLAYREADY_KEY_SYSTEM] &&
options.keySystems[PLAYREADY_KEY_SYSTEM].pssh) {
initData = options.keySystems[PLAYREADY_KEY_SYSTEM].pssh;
}
if (!initData) {
return;
}
sessions.push({ playready: true, initData });
msPrefixed({
video: event.target,
initData,
options,
eventBus
});
};
export const getOptions = (player) => {
return merge(player.currentSource(), player.eme.options);
};
/**
* Configure a persistent sessions array and activeSrc property to ensure we properly
* handle each independent source's events. Should be run on any encrypted or needkey
* style event to ensure that the sessions reflect the active source.
*
* @function setupSessions
* @param {Player} player
*/
export const setupSessions = (player) => {
const src = player.src();
if (src !== player.eme.activeSrc) {
player.eme.activeSrc = src;
player.eme.sessions = [];
}
};
/**
* Construct a simple function that can be used to dispatch EME errors on the
* player directly, such as providing it to a `.catch()`.
*
* @function emeErrorHandler
* @param {Player} player
* @return {Function}
*/
export const emeErrorHandler = (player) => {
return (objOrErr) => {
const error = {
// MEDIA_ERR_ENCRYPTED is code 5
code: 5
};
if (typeof objOrErr === 'string') {
error.message = objOrErr;
} else if (objOrErr) {
if (objOrErr.message) {
error.message = objOrErr.message;
}
if (objOrErr.cause &&
(objOrErr.cause.length ||
objOrErr.cause.byteLength)) {
error.cause = objOrErr.cause;
}
}
player.error(error);
};
};
/**
* Function to invoke when the player is ready.
*
* This is a great place for your plugin to initialize itself. When this
* function is called, the player will have its DOM and child components
* in place.
*
* @function onPlayerReady
* @param {Player} player
* @param {Function} emeError
*/
const onPlayerReady = (player, emeError) => {
if (player.$('.vjs-tech').tagName.toLowerCase() !== 'video') {
return;
}
setupSessions(player);
if (window.MediaKeys) {
// 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');
setupSessions(player);
handleEncryptedEvent(player, event, getOptions(player), 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);
}
});
} 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');
setupSessions(player);
try {
handleMsNeedKeyEvent(event, getOptions(player), player.eme.sessions, player.tech_);
} catch (error) {
emeError(error);
}
});
player.tech_.on('mskeyerror', emeError);
// TODO: refactor this plugin so it can use a plugin dispose
player.on('dispose', () => {
player.tech_.off('mskeyerror', emeError);
});
}
};
/**
* A video.js plugin.
*
* In the plugin function, the value of `this` is a video.js `Player`
* instance. You cannot rely on the player being in a "ready" state here,
* depending on how the plugin is invoked. This may or may not be important
* to you; if not, remove the wait for "ready"!
*
* @function eme
* @param {Object} [options={}]
* An object of options left to the plugin author to define.
*/
const eme = function(options = {}) {
const player = this;
const emeError = emeErrorHandler(player);
player.ready(() => onPlayerReady(player, emeError));
// Plugin API
player.eme = {
/**
* Sets up MediaKeys on demand
* Works around https://bugs.chromium.org/p/chromium/issues/detail?id=895449
*
* @function initializeMediaKeys
* @param {Object} [emeOptions={}]
* An object of eme plugin options.
* @param {Function} [callback=function(){}]
* @param {boolean} [suppressErrorIfPossible=false]
*/
initializeMediaKeys(emeOptions = {}, callback = function() {}, suppressErrorIfPossible = false) {
// TODO: this should be refactored and renamed to be less tied
// to encrypted events
const mergedEmeOptions = merge(
player.currentSource(),
options,
emeOptions
);
// fake an encrypted event for handleEncryptedEvent
const mockEncryptedEvent = {
initDataType: 'cenc',
initData: null,
target: player.tech_.el_
};
setupSessions(player);
if (window.MediaKeys) {
handleEncryptedEvent(player, mockEncryptedEvent, mergedEmeOptions, player.eme.sessions, player.tech_)
.then(() => callback())
.catch((error) => {
callback(error);
if (!suppressErrorIfPossible) {
emeError(error);
}
});
} else if (window.MSMediaKeys) {
const msKeyHandler = (event) => {
player.tech_.off('mskeyadded', msKeyHandler);
player.tech_.off('mskeyerror', msKeyHandler);
if (event.type === 'mskeyerror') {
callback(event.target.error);
if (!suppressErrorIfPossible) {
emeError(event.message);
}
} else {
callback();
}
};
player.tech_.one('mskeyadded', msKeyHandler);
player.tech_.one('mskeyerror', msKeyHandler);
try {
handleMsNeedKeyEvent(mockEncryptedEvent, mergedEmeOptions, player.eme.sessions, player.tech_);
} catch (error) {
player.tech_.off('mskeyadded', msKeyHandler);
player.tech_.off('mskeyerror', msKeyHandler);
callback(error);
if (!suppressErrorIfPossible) {
emeError(error);
}
}
}
},
options
};
};
// Register the plugin with video.js.
videojs.registerPlugin('eme', eme);
// Include the version number.
eme.VERSION = VERSION;
export default eme;