@kkcompany/player 1.15.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -0
- package/core.js +2345 -0
- package/index.d.ts +87 -0
- package/index.esm.js +1916 -0
- package/index.js +14152 -0
- package/modules.d.ts +87 -0
- package/modules.js +8470 -0
- package/package.json +52 -0
- package/plugins.d.ts +5 -0
- package/plugins.js +8046 -0
- package/react.d.ts +148 -0
- package/react.js +9872 -0
package/core.js
ADDED
|
@@ -0,0 +1,2345 @@
|
|
|
1
|
+
import UAParser from 'ua-parser-js';
|
|
2
|
+
import 'core-js/proposals/relative-indexing-method';
|
|
3
|
+
|
|
4
|
+
/* eslint-disable no-param-reassign */
|
|
5
|
+
const loadNative = ({
|
|
6
|
+
videoElement
|
|
7
|
+
}) => ({
|
|
8
|
+
load: ({
|
|
9
|
+
native: url
|
|
10
|
+
}) => {
|
|
11
|
+
videoElement.src = url;
|
|
12
|
+
videoElement.style.height = '100%';
|
|
13
|
+
videoElement.style.width = '100%';
|
|
14
|
+
},
|
|
15
|
+
play: () => videoElement.play(),
|
|
16
|
+
pause: () => videoElement.pause(),
|
|
17
|
+
seek: time => {
|
|
18
|
+
videoElement.currentTime = time;
|
|
19
|
+
},
|
|
20
|
+
getVideoElement: () => videoElement,
|
|
21
|
+
getVideoQuality: () => ({}),
|
|
22
|
+
destroy: () => {}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
We overwrite standard function for getting mediaSource object
|
|
27
|
+
because Chrome supports VideoTrack only in experiment mode.
|
|
28
|
+
*/
|
|
29
|
+
const getUrlObject = fn => {
|
|
30
|
+
const createObjectURL = window.URL.createObjectURL.bind();
|
|
31
|
+
|
|
32
|
+
window.URL.createObjectURL = blob => {
|
|
33
|
+
if (blob.addSourceBuffer) {
|
|
34
|
+
fn(blob);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return createObjectURL(blob);
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/*! @license
|
|
42
|
+
* Shaka Player
|
|
43
|
+
* Copyright 2016 Google LLC
|
|
44
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
45
|
+
*/
|
|
46
|
+
let shaka$1;
|
|
47
|
+
const shakaLog = {
|
|
48
|
+
v1: () => {}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const asMap = object => {
|
|
52
|
+
const map = new Map();
|
|
53
|
+
|
|
54
|
+
for (const key of Object.keys(object)) {
|
|
55
|
+
map.set(key, object[key]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return map;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const makeResponse = (headers, data, status, uri, responseURL, requestType) => {
|
|
62
|
+
if (status >= 200 && status <= 299 && status != 202) {
|
|
63
|
+
// Most 2xx HTTP codes are success cases.
|
|
64
|
+
|
|
65
|
+
/** @type {shaka.extern.Response} */
|
|
66
|
+
const response = {
|
|
67
|
+
uri: responseURL || uri,
|
|
68
|
+
originalUri: uri,
|
|
69
|
+
data,
|
|
70
|
+
status,
|
|
71
|
+
headers,
|
|
72
|
+
fromCache: !!headers['x-shaka-from-cache']
|
|
73
|
+
};
|
|
74
|
+
return response;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let responseText = null;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
responseText = shaka$1.util.StringUtils.fromBytesAutoDetect(data); // eslint-disable-next-line no-empty
|
|
81
|
+
} catch (exception) {}
|
|
82
|
+
|
|
83
|
+
const severity = status == 401 || status == 403 ? shaka$1.util.Error.Severity.CRITICAL : shaka$1.util.Error.Severity.RECOVERABLE;
|
|
84
|
+
throw new shaka$1.util.Error(severity, shaka$1.util.Error.Category.NETWORK, shaka$1.util.Error.Code.BAD_HTTP_STATUS, uri, status, responseText, headers, requestType);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const goog$1 = {
|
|
88
|
+
asserts: {
|
|
89
|
+
assert: () => {}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* @summary A networking plugin to handle http and https URIs via the Fetch API.
|
|
94
|
+
* @export
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
class HttpFetchPlugin {
|
|
98
|
+
/**
|
|
99
|
+
* @param {string} uri
|
|
100
|
+
* @param {shaka.extern.Request} request
|
|
101
|
+
* @param {shaka.net.NetworkingEngine.RequestType} requestType
|
|
102
|
+
* @param {shaka.extern.ProgressUpdated} progressUpdated Called when a
|
|
103
|
+
* progress event happened.
|
|
104
|
+
* @param {shaka.extern.HeadersReceived} headersReceived Called when the
|
|
105
|
+
* headers for the download are received, but before the body is.
|
|
106
|
+
* @return {!shaka.extern.IAbortableOperation.<shaka.extern.Response>}
|
|
107
|
+
* @export
|
|
108
|
+
*/
|
|
109
|
+
static parse(uri, request, requestType, progressUpdated, headersReceived) {
|
|
110
|
+
const headers = new HttpFetchPlugin.Headers_();
|
|
111
|
+
asMap(request.headers).forEach((value, key) => {
|
|
112
|
+
headers.append(key, value);
|
|
113
|
+
});
|
|
114
|
+
const controller = new HttpFetchPlugin.AbortController_();
|
|
115
|
+
/** @type {!RequestInit} */
|
|
116
|
+
|
|
117
|
+
const init = {
|
|
118
|
+
// Edge does not treat null as undefined for body; https://bit.ly/2luyE6x
|
|
119
|
+
body: request.body || undefined,
|
|
120
|
+
headers,
|
|
121
|
+
method: request.method,
|
|
122
|
+
signal: controller.signal,
|
|
123
|
+
credentials: request.allowCrossSiteCredentials ? 'include' : undefined
|
|
124
|
+
};
|
|
125
|
+
/** @type {shaka.net.HttpFetchPlugin.AbortStatus} */
|
|
126
|
+
|
|
127
|
+
const abortStatus = {
|
|
128
|
+
canceled: false,
|
|
129
|
+
timedOut: false
|
|
130
|
+
};
|
|
131
|
+
const pendingRequest = HttpFetchPlugin.request_(uri, requestType, init, abortStatus, progressUpdated, headersReceived, request.streamDataCallback);
|
|
132
|
+
/** @type {!shaka.util.AbortableOperation} */
|
|
133
|
+
|
|
134
|
+
const op = new shaka$1.util.AbortableOperation(pendingRequest, () => {
|
|
135
|
+
abortStatus.canceled = true;
|
|
136
|
+
controller.abort();
|
|
137
|
+
return Promise.resolve();
|
|
138
|
+
}); // The fetch API does not timeout natively, so do a timeout manually using
|
|
139
|
+
// the AbortController.
|
|
140
|
+
|
|
141
|
+
const timeoutMs = request.retryParameters.timeout;
|
|
142
|
+
|
|
143
|
+
if (timeoutMs) {
|
|
144
|
+
const timer = new shaka$1.util.Timer(() => {
|
|
145
|
+
abortStatus.timedOut = true;
|
|
146
|
+
controller.abort();
|
|
147
|
+
});
|
|
148
|
+
timer.tickAfter(timeoutMs / 1000); // To avoid calling |abort| on the network request after it finished, we
|
|
149
|
+
// will stop the timer when the requests resolves/rejects.
|
|
150
|
+
|
|
151
|
+
op.finally(() => {
|
|
152
|
+
timer.stop();
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return op;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* @param {string} uri
|
|
160
|
+
* @param {shaka.net.NetworkingEngine.RequestType} requestType
|
|
161
|
+
* @param {!RequestInit} init
|
|
162
|
+
* @param {shaka.net.HttpFetchPlugin.AbortStatus} abortStatus
|
|
163
|
+
* @param {shaka.extern.ProgressUpdated} progressUpdated
|
|
164
|
+
* @param {shaka.extern.HeadersReceived} headersReceived
|
|
165
|
+
* @param {?function(BufferSource):!Promise} streamDataCallback
|
|
166
|
+
* @return {!Promise<!shaka.extern.Response>}
|
|
167
|
+
* @private
|
|
168
|
+
*/
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
static async request_(uri, requestType, init, abortStatus, progressUpdated, headersReceived, streamDataCallback) {
|
|
172
|
+
const fetch = HttpFetchPlugin.fetch_;
|
|
173
|
+
const ReadableStream = HttpFetchPlugin.ReadableStream_;
|
|
174
|
+
let response;
|
|
175
|
+
let arrayBuffer;
|
|
176
|
+
let loaded = 0;
|
|
177
|
+
let lastLoaded = 0; // Last time stamp when we got a progress event.
|
|
178
|
+
|
|
179
|
+
let lastTime = Date.now();
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
// The promise returned by fetch resolves as soon as the HTTP response
|
|
183
|
+
// headers are available. The download itself isn't done until the promise
|
|
184
|
+
// for retrieving the data (arrayBuffer, blob, etc) has resolved.
|
|
185
|
+
response = await fetch(uri, init); // At this point in the process, we have the headers of the response, but
|
|
186
|
+
// not the body yet.
|
|
187
|
+
|
|
188
|
+
headersReceived(HttpFetchPlugin.headersToGenericObject_(response.headers)); // Getting the reader in this way allows us to observe the process of
|
|
189
|
+
// downloading the body, instead of just waiting for an opaque promise to
|
|
190
|
+
// resolve.
|
|
191
|
+
// We first clone the response because calling getReader locks the body
|
|
192
|
+
// stream; if we didn't clone it here, we would be unable to get the
|
|
193
|
+
// response's arrayBuffer later.
|
|
194
|
+
|
|
195
|
+
const reader = response.clone().body.getReader();
|
|
196
|
+
const contentLengthRaw = response.headers.get('Content-Length');
|
|
197
|
+
const contentLength = contentLengthRaw ? parseInt(contentLengthRaw, 10) : 0;
|
|
198
|
+
|
|
199
|
+
const start = controller => {
|
|
200
|
+
const push = async () => {
|
|
201
|
+
let readObj;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
readObj = await reader.read();
|
|
205
|
+
} catch (e) {
|
|
206
|
+
// If we abort the request, we'll get an error here. Just ignore it
|
|
207
|
+
// since real errors will be reported when we read the buffer below.
|
|
208
|
+
shakaLog.v1('error reading from stream', e.message);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!readObj.done) {
|
|
213
|
+
loaded += readObj.value.byteLength; // streamDataCallback adds stream data to buffer for low latency mode
|
|
214
|
+
// 4xx response means a segment is not ready and can retry soon
|
|
215
|
+
// only successful response data should be added, or playback freezes
|
|
216
|
+
|
|
217
|
+
if (response.status === 200 && streamDataCallback) {
|
|
218
|
+
await streamDataCallback(readObj.value);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const currentTime = Date.now(); // If the time between last time and this time we got progress event
|
|
223
|
+
// is long enough, or if a whole segment is downloaded, call
|
|
224
|
+
// progressUpdated().
|
|
225
|
+
|
|
226
|
+
if (currentTime - lastTime > 100 || readObj.done) {
|
|
227
|
+
progressUpdated(currentTime - lastTime, loaded - lastLoaded, contentLength - loaded);
|
|
228
|
+
lastLoaded = loaded;
|
|
229
|
+
lastTime = currentTime;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (readObj.done) {
|
|
233
|
+
goog$1.asserts.assert(!readObj.value, 'readObj should be unset when "done" is true.');
|
|
234
|
+
controller.close();
|
|
235
|
+
} else {
|
|
236
|
+
controller.enqueue(readObj.value);
|
|
237
|
+
push();
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
push();
|
|
242
|
+
}; // Create a ReadableStream to use the reader. We don't need to use the
|
|
243
|
+
// actual stream for anything, though, as we are using the response's
|
|
244
|
+
// arrayBuffer method to get the body, so we don't store the
|
|
245
|
+
// ReadableStream.
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
new ReadableStream({
|
|
249
|
+
start
|
|
250
|
+
}); // eslint-disable-line no-new
|
|
251
|
+
|
|
252
|
+
arrayBuffer = await response.arrayBuffer();
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (abortStatus.canceled) {
|
|
255
|
+
throw new shaka$1.util.Error(shaka$1.util.Error.Severity.RECOVERABLE, shaka$1.util.Error.Category.NETWORK, shaka$1.util.Error.Code.OPERATION_ABORTED, uri, requestType);
|
|
256
|
+
} else if (abortStatus.timedOut) {
|
|
257
|
+
throw new shaka$1.util.Error(shaka$1.util.Error.Severity.RECOVERABLE, shaka$1.util.Error.Category.NETWORK, shaka$1.util.Error.Code.TIMEOUT, uri, requestType);
|
|
258
|
+
} else {
|
|
259
|
+
throw new shaka$1.util.Error(shaka$1.util.Error.Severity.RECOVERABLE, shaka$1.util.Error.Category.NETWORK, shaka$1.util.Error.Code.HTTP_ERROR, uri, error, requestType);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const headers = HttpFetchPlugin.headersToGenericObject_(response.headers);
|
|
264
|
+
return makeResponse(headers, arrayBuffer, response.status, uri, response.url, requestType);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* @param {!Headers} headers
|
|
268
|
+
* @return {!Object.<string, string>}
|
|
269
|
+
* @private
|
|
270
|
+
*/
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
static headersToGenericObject_(headers) {
|
|
274
|
+
const headersObj = {};
|
|
275
|
+
headers.forEach((value, key) => {
|
|
276
|
+
// Since Edge incorrectly return the header with a leading new line
|
|
277
|
+
// character ('\n'), we trim the header here.
|
|
278
|
+
headersObj[key.trim()] = value;
|
|
279
|
+
});
|
|
280
|
+
return headersObj;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
HttpFetchPlugin.register = shakaNamespace => {
|
|
286
|
+
shaka$1 = shakaNamespace;
|
|
287
|
+
/**
|
|
288
|
+
* Overridden in unit tests, but compiled out in production.
|
|
289
|
+
*
|
|
290
|
+
* @const {function(string, !RequestInit)}
|
|
291
|
+
* @private
|
|
292
|
+
*/
|
|
293
|
+
|
|
294
|
+
HttpFetchPlugin.fetch_ = window.fetch;
|
|
295
|
+
/**
|
|
296
|
+
* Overridden in unit tests, but compiled out in production.
|
|
297
|
+
*
|
|
298
|
+
* @const {function(new: AbortController)}
|
|
299
|
+
* @private
|
|
300
|
+
*/
|
|
301
|
+
|
|
302
|
+
HttpFetchPlugin.AbortController_ = window.AbortController;
|
|
303
|
+
/**
|
|
304
|
+
* Overridden in unit tests, but compiled out in production.
|
|
305
|
+
*
|
|
306
|
+
* @const {function(new: ReadableStream, !Object)}
|
|
307
|
+
* @private
|
|
308
|
+
*/
|
|
309
|
+
|
|
310
|
+
HttpFetchPlugin.ReadableStream_ = window.ReadableStream;
|
|
311
|
+
/**
|
|
312
|
+
* Overridden in unit tests, but compiled out in production.
|
|
313
|
+
*
|
|
314
|
+
* @const {function(new: Headers)}
|
|
315
|
+
* @private
|
|
316
|
+
*/
|
|
317
|
+
|
|
318
|
+
HttpFetchPlugin.Headers_ = window.Headers;
|
|
319
|
+
shaka$1.net.NetworkingEngine.registerScheme('http', HttpFetchPlugin.parse);
|
|
320
|
+
shaka$1.net.NetworkingEngine.registerScheme('https', HttpFetchPlugin.parse);
|
|
321
|
+
shaka$1.net.NetworkingEngine.registerScheme('blob', HttpFetchPlugin.parse);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
/* eslint-disable guard-for-in */
|
|
325
|
+
|
|
326
|
+
/* eslint-disable no-unused-vars */
|
|
327
|
+
const myLog = console;
|
|
328
|
+
const goog = {
|
|
329
|
+
asserts: {
|
|
330
|
+
assert: (result, message) => result || console.warn('message')
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
const ALL_EVENTS_ = 'All';
|
|
334
|
+
|
|
335
|
+
function PublicPromise() {
|
|
336
|
+
let resolvePromise;
|
|
337
|
+
let rejectPromise;
|
|
338
|
+
const promise = new Promise((resolve, reject) => {
|
|
339
|
+
resolvePromise = resolve;
|
|
340
|
+
rejectPromise = reject;
|
|
341
|
+
});
|
|
342
|
+
this.resolve = resolvePromise;
|
|
343
|
+
this.reject = rejectPromise;
|
|
344
|
+
|
|
345
|
+
this.then = (...args) => promise.then(...args);
|
|
346
|
+
|
|
347
|
+
this.catch = (...args) => promise.catch(...args);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
class MultiMap {
|
|
351
|
+
/** */
|
|
352
|
+
constructor() {
|
|
353
|
+
/** @private {!Object.<string, !Array.<T>>} */
|
|
354
|
+
this.map_ = {};
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Add a key, value pair to the map.
|
|
358
|
+
* @param {string} key
|
|
359
|
+
* @param {T} value
|
|
360
|
+
*/
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
push(key, value) {
|
|
364
|
+
// eslint-disable-next-line no-prototype-builtins
|
|
365
|
+
if (this.map_.hasOwnProperty(key)) {
|
|
366
|
+
this.map_[key].push(value);
|
|
367
|
+
} else {
|
|
368
|
+
this.map_[key] = [value];
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Get a list of values by key.
|
|
373
|
+
* @param {string} key
|
|
374
|
+
* @return {Array.<T>} or null if no such key exists.
|
|
375
|
+
*/
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
get(key) {
|
|
379
|
+
const list = this.map_[key]; // slice() clones the list so that it and the map can each be modified
|
|
380
|
+
// without affecting the other.
|
|
381
|
+
|
|
382
|
+
return list ? list.slice() : null;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Get a list of all values.
|
|
386
|
+
* @return {!Array.<T>}
|
|
387
|
+
*/
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
getAll() {
|
|
391
|
+
const list = [];
|
|
392
|
+
|
|
393
|
+
for (const key in this.map_) {
|
|
394
|
+
list.push(...this.map_[key]);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return list;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Remove a specific value, if it exists.
|
|
401
|
+
* @param {string} key
|
|
402
|
+
* @param {T} value
|
|
403
|
+
*/
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
remove(key, value) {
|
|
407
|
+
if (!(key in this.map_)) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
this.map_[key] = this.map_[key].filter(i => i != value);
|
|
412
|
+
|
|
413
|
+
if (this.map_[key].length == 0) {
|
|
414
|
+
// Delete the array if it's empty, so that |get| will reliably return null
|
|
415
|
+
// "if no such key exists", instead of sometimes returning an empty array.
|
|
416
|
+
delete this.map_[key];
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Clear all keys and values from the multimap.
|
|
421
|
+
*/
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
clear() {
|
|
425
|
+
this.map_ = {};
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* @param {function(string, !Array.<T>)} callback
|
|
429
|
+
*/
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
forEach(callback) {
|
|
433
|
+
for (const key in this.map_) {
|
|
434
|
+
callback(key, this.map_[key]);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Returns the number of elements in the multimap.
|
|
439
|
+
* @return {number}
|
|
440
|
+
*/
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
size() {
|
|
444
|
+
return Object.keys(this.map_).length;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Get a list of all the keys.
|
|
448
|
+
* @return {!Array.<string>}
|
|
449
|
+
*/
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
keys() {
|
|
453
|
+
return Object.keys(this.map_);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
class FakeEventTarget {
|
|
459
|
+
/** */
|
|
460
|
+
constructor() {
|
|
461
|
+
/**
|
|
462
|
+
* @private {shaka.util.MultiMap.<shaka.util.FakeEventTarget.ListenerType>}
|
|
463
|
+
*/
|
|
464
|
+
this.listeners_ = new MultiMap();
|
|
465
|
+
/**
|
|
466
|
+
* The target of all dispatched events. Defaults to |this|.
|
|
467
|
+
* @type {EventTarget}
|
|
468
|
+
*/
|
|
469
|
+
|
|
470
|
+
this.dispatchTarget = this;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Add an event listener to this object.
|
|
474
|
+
*
|
|
475
|
+
* @param {string} type The event type to listen for.
|
|
476
|
+
* @param {shaka.util.FakeEventTarget.ListenerType} listener The callback or
|
|
477
|
+
* listener object to invoke.
|
|
478
|
+
* @param {(!AddEventListenerOptions|boolean)=} options Ignored.
|
|
479
|
+
* @override
|
|
480
|
+
* @exportInterface
|
|
481
|
+
*/
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
addEventListener(type, listener, options) {
|
|
485
|
+
if (!this.listeners_) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
this.listeners_.push(type, listener);
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Add an event listener to this object that is invoked for all events types
|
|
493
|
+
* the object fires.
|
|
494
|
+
*
|
|
495
|
+
* @param {shaka.util.FakeEventTarget.ListenerType} listener The callback or
|
|
496
|
+
* listener object to invoke.
|
|
497
|
+
* @exportInterface
|
|
498
|
+
*/
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
listenToAllEvents(listener) {
|
|
502
|
+
this.addEventListener(ALL_EVENTS_, listener);
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Remove an event listener from this object.
|
|
506
|
+
*
|
|
507
|
+
* @param {string} type The event type for which you wish to remove a
|
|
508
|
+
* listener.
|
|
509
|
+
* @param {shaka.util.FakeEventTarget.ListenerType} listener The callback or
|
|
510
|
+
* listener object to remove.
|
|
511
|
+
* @param {(EventListenerOptions|boolean)=} options Ignored.
|
|
512
|
+
* @override
|
|
513
|
+
* @exportInterface
|
|
514
|
+
*/
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
removeEventListener(type, listener, options) {
|
|
518
|
+
if (!this.listeners_) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
this.listeners_.remove(type, listener);
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Dispatch an event from this object.
|
|
526
|
+
*
|
|
527
|
+
* @param {!Event} event The event to be dispatched from this object.
|
|
528
|
+
* @return {boolean} True if the default action was prevented.
|
|
529
|
+
* @override
|
|
530
|
+
* @exportInterface
|
|
531
|
+
*/
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
dispatchEvent(event) {
|
|
535
|
+
// In many browsers, it is complex to overwrite properties of actual Events.
|
|
536
|
+
// Here we expect only to dispatch FakeEvents, which are simpler.
|
|
537
|
+
goog.asserts.assert(event instanceof shaka.util.FakeEvent, 'FakeEventTarget can only dispatch FakeEvents!');
|
|
538
|
+
|
|
539
|
+
if (!this.listeners_) {
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
let listeners = this.listeners_.get(event.type) || [];
|
|
544
|
+
const universalListeners = this.listeners_.get(ALL_EVENTS_);
|
|
545
|
+
|
|
546
|
+
if (universalListeners) {
|
|
547
|
+
listeners = listeners.concat(universalListeners);
|
|
548
|
+
} // Execute this event on listeners until the event has been stopped or we
|
|
549
|
+
// run out of listeners.
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
for (const listener of listeners) {
|
|
553
|
+
// Do this every time, since events can be re-dispatched from handlers.
|
|
554
|
+
event.target = this.dispatchTarget;
|
|
555
|
+
event.currentTarget = this.dispatchTarget;
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
// Check for the |handleEvent| member to test if this is a
|
|
559
|
+
// |EventListener| instance or a basic function.
|
|
560
|
+
if (listener.handleEvent) {
|
|
561
|
+
listener.handleEvent(event);
|
|
562
|
+
} else {
|
|
563
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
564
|
+
listener.call(this, event);
|
|
565
|
+
}
|
|
566
|
+
} catch (exception) {
|
|
567
|
+
// Exceptions during event handlers should not affect the caller,
|
|
568
|
+
// but should appear on the console as uncaught, according to MDN:
|
|
569
|
+
// https://mzl.la/2JXgwRo
|
|
570
|
+
myLog.error('Uncaught exception in event handler', exception, exception ? exception.message : null, exception ? exception.stack : null);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (event.stopped) {
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return event.defaultPrevented;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* @override
|
|
582
|
+
* @exportInterface
|
|
583
|
+
*/
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
release() {
|
|
587
|
+
this.listeners_ = null;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const waitForReadyState = (mediaElement, readyState, eventManager, callback) => {
|
|
593
|
+
const READY_STATES_TO_EVENT_NAMES_ = [[window.HTMLMediaElement.HAVE_METADATA, 'loadedmetadata'], [window.HTMLMediaElement.HAVE_CURRENT_DATA, 'loadeddata'], [window.HTMLMediaElement.HAVE_FUTURE_DATA, 'canplay'], [window.HTMLMediaElement.HAVE_ENOUGH_DATA, 'canplaythrough']];
|
|
594
|
+
|
|
595
|
+
if (readyState == window.HTMLMediaElement.HAVE_NOTHING || mediaElement.readyState >= readyState) {
|
|
596
|
+
callback();
|
|
597
|
+
} else {
|
|
598
|
+
const eventName = READY_STATES_TO_EVENT_NAMES_.find(x => x[0] === readyState)[1];
|
|
599
|
+
eventManager.listenOnce(mediaElement, eventName, callback);
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
class PatchedMediaKeysApple {
|
|
604
|
+
/**
|
|
605
|
+
* Installs the polyfill if needed.
|
|
606
|
+
* @export
|
|
607
|
+
*/
|
|
608
|
+
static install(shaka) {
|
|
609
|
+
if (!window.HTMLVideoElement || !window.WebKitMediaKeys) {
|
|
610
|
+
// No HTML5 video or no prefixed EME.
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
myLog.info('Using Apple-prefixed EME'); // Delete mediaKeys to work around strict mode compatibility issues.
|
|
615
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
616
|
+
|
|
617
|
+
delete window.HTMLMediaElement.prototype.mediaKeys; // Work around read-only declaration for mediaKeys by using a string.
|
|
618
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
619
|
+
|
|
620
|
+
window.HTMLMediaElement.prototype.mediaKeys = null; // eslint-disable-next-line no-restricted-syntax
|
|
621
|
+
|
|
622
|
+
window.HTMLMediaElement.prototype.setMediaKeys = PatchedMediaKeysApple.setMediaKeys; // Install patches
|
|
623
|
+
|
|
624
|
+
window.MediaKeys = PatchedMediaKeysApple.MediaKeys;
|
|
625
|
+
window.MediaKeySystemAccess = PatchedMediaKeysApple.MediaKeySystemAccess;
|
|
626
|
+
navigator.requestMediaKeySystemAccess = PatchedMediaKeysApple.requestMediaKeySystemAccess;
|
|
627
|
+
window.shakaMediaKeysPolyfill = true;
|
|
628
|
+
|
|
629
|
+
const defaultInitDataTransform = (initData, initDataType, drmInfo) => {
|
|
630
|
+
if (initDataType === 'skd') {
|
|
631
|
+
const {
|
|
632
|
+
defaultGetContentId,
|
|
633
|
+
initDataTransform
|
|
634
|
+
} = shaka.util.FairPlayUtils;
|
|
635
|
+
const cert = drmInfo.serverCertificate;
|
|
636
|
+
const contentId = defaultGetContentId(initData);
|
|
637
|
+
return initDataTransform(initData, contentId, cert);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return initData;
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const setupPlayer = player => {
|
|
644
|
+
player.licenseRequestHandler = request => {
|
|
645
|
+
const base64Payload = encodeURIComponent(btoa(String.fromCharCode(...new Uint8Array(request.body))));
|
|
646
|
+
const contentId = encodeURIComponent(new TextDecoder('utf-8').decode(request.initData).slice(6));
|
|
647
|
+
request.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
648
|
+
request.body = `spc=${base64Payload}&asset_id=${contentId}`;
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
player.configure({
|
|
652
|
+
drm: {
|
|
653
|
+
initDataTransform: defaultInitDataTransform
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
player.getNetworkingEngine().registerResponseFilter((type, response) => {
|
|
657
|
+
if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const responseText = new TextDecoder('utf-8').decode(response.data).trim();
|
|
662
|
+
|
|
663
|
+
if (responseText.slice(0, 5) === '<ckc>' && responseText.slice(-6) === '</ckc>') {
|
|
664
|
+
response.data = Uint8Array.from(atob(responseText.slice(5, -6)), c => c.charCodeAt(0));
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
PatchedMediaKeysApple.setupPlayer = setupPlayer;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* An implementation of navigator.requestMediaKeySystemAccess.
|
|
673
|
+
* Retrieves a MediaKeySystemAccess object.
|
|
674
|
+
*
|
|
675
|
+
* @this {!Navigator}
|
|
676
|
+
* @param {string} keySystem
|
|
677
|
+
* @param {!Array.<!MediaKeySystemConfiguration>} supportedConfigurations
|
|
678
|
+
* @return {!Promise.<!MediaKeySystemAccess>}
|
|
679
|
+
*/
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
static requestMediaKeySystemAccess(keySystem, supportedConfigurations) {
|
|
683
|
+
myLog.debug('PatchedMediaKeysApple.requestMediaKeySystemAccess');
|
|
684
|
+
console.info({
|
|
685
|
+
keySystem,
|
|
686
|
+
supportedConfigurations
|
|
687
|
+
});
|
|
688
|
+
goog.asserts.assert(this == navigator, 'bad "this" for requestMediaKeySystemAccess');
|
|
689
|
+
|
|
690
|
+
try {
|
|
691
|
+
console.info({
|
|
692
|
+
keySystem,
|
|
693
|
+
supportedConfigurations
|
|
694
|
+
});
|
|
695
|
+
const access = new PatchedMediaKeysApple.MediaKeySystemAccess(keySystem, supportedConfigurations);
|
|
696
|
+
return Promise.resolve(
|
|
697
|
+
/** @type {!MediaKeySystemAccess} */
|
|
698
|
+
access);
|
|
699
|
+
} catch (exception) {
|
|
700
|
+
console.error(exception);
|
|
701
|
+
return Promise.reject(exception);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* An implementation of window.HTMLMediaElement.prototype.setMediaKeys.
|
|
706
|
+
* Attaches a MediaKeys object to the media element.
|
|
707
|
+
*
|
|
708
|
+
* @this {!window.HTMLMediaElement}
|
|
709
|
+
* @param {MediaKeys} mediaKeys
|
|
710
|
+
* @return {!Promise}
|
|
711
|
+
*/
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
static setMediaKeys(mediaKeys) {
|
|
715
|
+
myLog.debug('PatchedMediaKeysApple.setMediaKeys');
|
|
716
|
+
goog.asserts.assert(this instanceof window.HTMLMediaElement, 'bad "this" for setMediaKeys');
|
|
717
|
+
const newMediaKeys =
|
|
718
|
+
/** @type {window.shaka.polyfill.PatchedMediaKeysApple.MediaKeys} */
|
|
719
|
+
mediaKeys;
|
|
720
|
+
const oldMediaKeys =
|
|
721
|
+
/** @type {window.shaka.polyfill.PatchedMediaKeysApple.MediaKeys} */
|
|
722
|
+
this.mediaKeys;
|
|
723
|
+
|
|
724
|
+
if (oldMediaKeys && oldMediaKeys != newMediaKeys) {
|
|
725
|
+
goog.asserts.assert(oldMediaKeys instanceof PatchedMediaKeysApple.MediaKeys, 'non-polyfill instance of oldMediaKeys'); // Have the old MediaKeys stop listening to events on the video tag.
|
|
726
|
+
|
|
727
|
+
oldMediaKeys.setMedia(null);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
delete this.mediaKeys; // in case there is an existing getter
|
|
731
|
+
|
|
732
|
+
this.mediaKeys = mediaKeys; // work around read-only declaration
|
|
733
|
+
|
|
734
|
+
if (newMediaKeys) {
|
|
735
|
+
goog.asserts.assert(newMediaKeys instanceof PatchedMediaKeysApple.MediaKeys, 'non-polyfill instance of newMediaKeys');
|
|
736
|
+
return newMediaKeys.setMedia(this);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return Promise.resolve();
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Handler for the native media elements webkitneedkey event.
|
|
743
|
+
*
|
|
744
|
+
* @this {!window.HTMLMediaElement}
|
|
745
|
+
* @param {!MediaKeyEvent} event
|
|
746
|
+
* @suppress {constantProperty} We reassign what would be const on a real
|
|
747
|
+
* MediaEncryptedEvent, but in our look-alike event.
|
|
748
|
+
* @private
|
|
749
|
+
*/
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
static onWebkitNeedKey_(event) {
|
|
753
|
+
myLog.debug('PatchedMediaKeysApple.onWebkitNeedKey_', event);
|
|
754
|
+
const {
|
|
755
|
+
mediaKeys
|
|
756
|
+
} = this;
|
|
757
|
+
goog.asserts.assert(mediaKeys instanceof PatchedMediaKeysApple.MediaKeys, 'non-polyfill instance of newMediaKeys');
|
|
758
|
+
goog.asserts.assert(event.initData != null, 'missing init data!'); // Convert the prefixed init data to match the native 'encrypted' event.
|
|
759
|
+
|
|
760
|
+
const uint8 = window.shaka.util.BufferUtils.toUint8(event.initData);
|
|
761
|
+
const dataview = window.shaka.util.BufferUtils.toDataView(uint8); // The first part is a 4 byte little-endian int, which is the length of
|
|
762
|
+
// the second part.
|
|
763
|
+
|
|
764
|
+
const length = dataview.getUint32(
|
|
765
|
+
/* position= */
|
|
766
|
+
0,
|
|
767
|
+
/* littleEndian= */
|
|
768
|
+
true);
|
|
769
|
+
|
|
770
|
+
if (length + 4 != uint8.byteLength) {
|
|
771
|
+
throw new RangeError('Malformed FairPlay init data');
|
|
772
|
+
} // The remainder is a UTF-16 skd URL. Convert this to UTF-8 and pass on.
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
const str = window.shaka.util.StringUtils.fromUTF16(uint8.subarray(4),
|
|
776
|
+
/* littleEndian= */
|
|
777
|
+
true);
|
|
778
|
+
const initData = window.shaka.util.StringUtils.toUTF8(str); // NOTE: Because "this" is a real EventTarget, the event we dispatch here
|
|
779
|
+
// must also be a real Event.
|
|
780
|
+
|
|
781
|
+
const event2 = new Event('encrypted');
|
|
782
|
+
const encryptedEvent =
|
|
783
|
+
/** @type {!MediaEncryptedEvent} */
|
|
784
|
+
|
|
785
|
+
/** @type {?} */
|
|
786
|
+
event2;
|
|
787
|
+
encryptedEvent.initDataType = 'skd';
|
|
788
|
+
encryptedEvent.initData = window.shaka.util.BufferUtils.toArrayBuffer(initData);
|
|
789
|
+
this.dispatchEvent(event2);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* An implementation of MediaKeySystemAccess.
|
|
795
|
+
*
|
|
796
|
+
* @implements {MediaKeySystemAccess}
|
|
797
|
+
*/
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
PatchedMediaKeysApple.MediaKeySystemAccess = class {
|
|
801
|
+
/**
|
|
802
|
+
* @param {string} keySystem
|
|
803
|
+
* @param {!Array.<!MediaKeySystemConfiguration>} supportedConfigurations
|
|
804
|
+
*/
|
|
805
|
+
constructor(keySystem, supportedConfigurations) {
|
|
806
|
+
myLog.debug('PatchedMediaKeysApple.MediaKeySystemAccess');
|
|
807
|
+
/** @type {string} */
|
|
808
|
+
|
|
809
|
+
this.keySystem = keySystem;
|
|
810
|
+
/** @private {!MediaKeySystemConfiguration} */
|
|
811
|
+
|
|
812
|
+
this.configuration_; // Optimization: WebKitMediaKeys.isTypeSupported delays responses by a
|
|
813
|
+
// significant amount of time, possibly to discourage fingerprinting.
|
|
814
|
+
// Since we know only FairPlay is supported here, let's skip queries for
|
|
815
|
+
// anything else to speed up the process.
|
|
816
|
+
|
|
817
|
+
if (keySystem.startsWith('com.apple.fps')) {
|
|
818
|
+
for (const cfg of supportedConfigurations) {
|
|
819
|
+
const newCfg = this.checkConfig_(cfg);
|
|
820
|
+
|
|
821
|
+
if (newCfg) {
|
|
822
|
+
this.configuration_ = newCfg;
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
} // According to the spec, this should be a DOMException, but there is not a
|
|
827
|
+
// public constructor for that. So we make this look-alike instead.
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
const unsupportedKeySystemError = new Error('Unsupported keySystem');
|
|
831
|
+
unsupportedKeySystemError.name = 'NotSupportedError';
|
|
832
|
+
unsupportedKeySystemError.code = DOMException.NOT_SUPPORTED_ERR;
|
|
833
|
+
throw unsupportedKeySystemError;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Check a single config for MediaKeySystemAccess.
|
|
837
|
+
*
|
|
838
|
+
* @param {MediaKeySystemConfiguration} cfg The requested config.
|
|
839
|
+
* @return {?MediaKeySystemConfiguration} A matching config we can support, or
|
|
840
|
+
* null if the input is not supportable.
|
|
841
|
+
* @private
|
|
842
|
+
*/
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
checkConfig_(cfg) {
|
|
846
|
+
if (cfg.persistentState == 'required') {
|
|
847
|
+
// Not supported by the prefixed API.
|
|
848
|
+
return null;
|
|
849
|
+
} // Create a new config object and start adding in the pieces which we find
|
|
850
|
+
// support for. We will return this from getConfiguration() later if
|
|
851
|
+
// asked.
|
|
852
|
+
|
|
853
|
+
/** @type {!MediaKeySystemConfiguration} */
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
const newCfg = {
|
|
857
|
+
audioCapabilities: [],
|
|
858
|
+
videoCapabilities: [],
|
|
859
|
+
// It is technically against spec to return these as optional, but we
|
|
860
|
+
// don't truly know their values from the prefixed API:
|
|
861
|
+
persistentState: 'optional',
|
|
862
|
+
distinctiveIdentifier: 'optional',
|
|
863
|
+
// Pretend the requested init data types are supported, since we don't
|
|
864
|
+
// really know that either:
|
|
865
|
+
initDataTypes: cfg.initDataTypes,
|
|
866
|
+
sessionTypes: ['temporary'],
|
|
867
|
+
label: cfg.label
|
|
868
|
+
}; // PatchedMediaKeysApple tests for key system availability through
|
|
869
|
+
// WebKitMediaKeys.isTypeSupported.
|
|
870
|
+
|
|
871
|
+
let ranAnyTests = false;
|
|
872
|
+
let success = false;
|
|
873
|
+
|
|
874
|
+
if (cfg.audioCapabilities) {
|
|
875
|
+
for (const cap of cfg.audioCapabilities) {
|
|
876
|
+
if (cap.contentType) {
|
|
877
|
+
ranAnyTests = true;
|
|
878
|
+
const contentType = cap.contentType.split(';')[0];
|
|
879
|
+
|
|
880
|
+
if (window.WebKitMediaKeys.isTypeSupported(this.keySystem, contentType)) {
|
|
881
|
+
newCfg.audioCapabilities.push(cap);
|
|
882
|
+
success = true;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (cfg.videoCapabilities) {
|
|
889
|
+
for (const cap of cfg.videoCapabilities) {
|
|
890
|
+
if (cap.contentType) {
|
|
891
|
+
ranAnyTests = true;
|
|
892
|
+
const contentType = cap.contentType.split(';')[0];
|
|
893
|
+
|
|
894
|
+
if (window.WebKitMediaKeys.isTypeSupported(this.keySystem, contentType)) {
|
|
895
|
+
newCfg.videoCapabilities.push(cap);
|
|
896
|
+
success = true;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (!ranAnyTests) {
|
|
903
|
+
// If no specific types were requested, we check all common types to
|
|
904
|
+
// find out if the key system is present at all.
|
|
905
|
+
success = window.WebKitMediaKeys.isTypeSupported(this.keySystem, 'video/mp4');
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (success) {
|
|
909
|
+
return newCfg;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
/** @override */
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
createMediaKeys() {
|
|
918
|
+
myLog.debug('PatchedMediaKeysApple.MediaKeySystemAccess.createMediaKeys');
|
|
919
|
+
const mediaKeys = new PatchedMediaKeysApple.MediaKeys(this.keySystem);
|
|
920
|
+
return Promise.resolve(
|
|
921
|
+
/** @type {!MediaKeys} */
|
|
922
|
+
mediaKeys);
|
|
923
|
+
}
|
|
924
|
+
/** @override */
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
getConfiguration() {
|
|
928
|
+
myLog.debug('PatchedMediaKeysApple.MediaKeySystemAccess.getConfiguration');
|
|
929
|
+
return this.configuration_;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
};
|
|
933
|
+
/**
|
|
934
|
+
* An implementation of MediaKeys.
|
|
935
|
+
*
|
|
936
|
+
* @implements {MediaKeys}
|
|
937
|
+
*/
|
|
938
|
+
|
|
939
|
+
PatchedMediaKeysApple.MediaKeys = class {
|
|
940
|
+
/** @param {string} keySystem */
|
|
941
|
+
constructor(keySystem) {
|
|
942
|
+
myLog.debug('PatchedMediaKeysApple.MediaKeys');
|
|
943
|
+
/** @private {!WebKitMediaKeys} */
|
|
944
|
+
|
|
945
|
+
this.nativeMediaKeys_ = new window.WebKitMediaKeys(keySystem);
|
|
946
|
+
/** @private {!window.shaka.util.EventManager} */
|
|
947
|
+
|
|
948
|
+
this.eventManager_ = new window.shaka.util.EventManager();
|
|
949
|
+
}
|
|
950
|
+
/** @override */
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
createSession(sessionType) {
|
|
954
|
+
myLog.debug('PatchedMediaKeysApple.MediaKeys.createSession');
|
|
955
|
+
sessionType = sessionType || 'temporary'; // For now, only the 'temporary' type is supported.
|
|
956
|
+
|
|
957
|
+
if (sessionType != 'temporary') {
|
|
958
|
+
throw new TypeError(`Session type ${sessionType} is unsupported on this platform.`);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return new PatchedMediaKeysApple.MediaKeySession(this.nativeMediaKeys_, sessionType);
|
|
962
|
+
}
|
|
963
|
+
/** @override */
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
setServerCertificate(serverCertificate) {
|
|
967
|
+
myLog.debug('PatchedMediaKeysApple.MediaKeys.setServerCertificate');
|
|
968
|
+
return Promise.resolve(false);
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* @param {window.HTMLMediaElement} media
|
|
972
|
+
* @protected
|
|
973
|
+
* @return {!Promise}
|
|
974
|
+
*/
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
setMedia(media) {
|
|
978
|
+
// Remove any old listeners.
|
|
979
|
+
this.eventManager_.removeAll(); // It is valid for media to be null; null is used to flag that event
|
|
980
|
+
// handlers need to be cleaned up.
|
|
981
|
+
|
|
982
|
+
if (!media) {
|
|
983
|
+
return Promise.resolve();
|
|
984
|
+
} // Intercept and translate these prefixed EME events.
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
this.eventManager_.listen(media, 'webkitneedkey',
|
|
988
|
+
/** @type {window.shaka.util.EventManager.ListenerType} */
|
|
989
|
+
PatchedMediaKeysApple.onWebkitNeedKey_); // Wrap native window.HTMLMediaElement.webkitSetMediaKeys with a Promise.
|
|
990
|
+
|
|
991
|
+
try {
|
|
992
|
+
// Some browsers require that readyState >=1 before mediaKeys can be
|
|
993
|
+
// set, so check this and wait for loadedmetadata if we are not in the
|
|
994
|
+
// correct state
|
|
995
|
+
waitForReadyState(media, window.HTMLMediaElement.HAVE_METADATA, this.eventManager_, () => {
|
|
996
|
+
media.webkitSetMediaKeys(this.nativeMediaKeys_);
|
|
997
|
+
});
|
|
998
|
+
return Promise.resolve();
|
|
999
|
+
} catch (exception) {
|
|
1000
|
+
return Promise.reject(exception);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
};
|
|
1005
|
+
/**
|
|
1006
|
+
* An implementation of MediaKeySession.
|
|
1007
|
+
*
|
|
1008
|
+
* @implements {MediaKeySession}
|
|
1009
|
+
*/
|
|
1010
|
+
|
|
1011
|
+
PatchedMediaKeysApple.MediaKeySession = class extends FakeEventTarget {
|
|
1012
|
+
/**
|
|
1013
|
+
* @param {WebKitMediaKeys} nativeMediaKeys
|
|
1014
|
+
* @param {string} sessionType
|
|
1015
|
+
*/
|
|
1016
|
+
constructor(nativeMediaKeys, sessionType) {
|
|
1017
|
+
myLog.debug('PatchedMediaKeysApple.MediaKeySession');
|
|
1018
|
+
super();
|
|
1019
|
+
/**
|
|
1020
|
+
* The native MediaKeySession, which will be created in generateRequest.
|
|
1021
|
+
* @private {WebKitMediaKeySession}
|
|
1022
|
+
*/
|
|
1023
|
+
|
|
1024
|
+
this.nativeMediaKeySession_ = null;
|
|
1025
|
+
/** @private {WebKitMediaKeys} */
|
|
1026
|
+
|
|
1027
|
+
this.nativeMediaKeys_ = nativeMediaKeys; // Promises that are resolved later
|
|
1028
|
+
|
|
1029
|
+
/** @private {PublicPromise} */
|
|
1030
|
+
|
|
1031
|
+
this.generateRequestPromise_ = null;
|
|
1032
|
+
/** @private {PublicPromise} */
|
|
1033
|
+
|
|
1034
|
+
this.updatePromise_ = null;
|
|
1035
|
+
/** @private {!window.shaka.util.EventManager} */
|
|
1036
|
+
|
|
1037
|
+
this.eventManager_ = new window.shaka.util.EventManager();
|
|
1038
|
+
/** @type {string} */
|
|
1039
|
+
|
|
1040
|
+
this.sessionId = '';
|
|
1041
|
+
/** @type {number} */
|
|
1042
|
+
|
|
1043
|
+
this.expiration = NaN;
|
|
1044
|
+
/** @type {!PublicPromise} */
|
|
1045
|
+
|
|
1046
|
+
this.closed = new PublicPromise();
|
|
1047
|
+
/** @type {!window.shaka.polyfill.PatchedMediaKeysApple.MediaKeyStatusMap} */
|
|
1048
|
+
|
|
1049
|
+
this.keyStatuses = new PatchedMediaKeysApple.MediaKeyStatusMap();
|
|
1050
|
+
}
|
|
1051
|
+
/** @override */
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
generateRequest(initDataType, initData) {
|
|
1055
|
+
myLog.debug('PatchedMediaKeysApple.MediaKeySession.generateRequest');
|
|
1056
|
+
this.generateRequestPromise_ = new PublicPromise();
|
|
1057
|
+
|
|
1058
|
+
try {
|
|
1059
|
+
// This EME spec version requires a MIME content type as the 1st param to
|
|
1060
|
+
// createSession, but doesn't seem to matter what the value is.
|
|
1061
|
+
// It also only accepts Uint8Array, not ArrayBuffer, so explicitly make
|
|
1062
|
+
// initData into a Uint8Array.
|
|
1063
|
+
const session = this.nativeMediaKeys_.createSession('video/mp4', window.shaka.util.BufferUtils.toUint8(initData));
|
|
1064
|
+
this.nativeMediaKeySession_ = session;
|
|
1065
|
+
this.sessionId = session.sessionId || ''; // Attach session event handlers here.
|
|
1066
|
+
|
|
1067
|
+
this.eventManager_.listen(this.nativeMediaKeySession_, 'webkitkeymessage',
|
|
1068
|
+
/** @type {window.shaka.util.EventManager.ListenerType} */
|
|
1069
|
+
event => this.onWebkitKeyMessage_(event));
|
|
1070
|
+
this.eventManager_.listen(session, 'webkitkeyadded',
|
|
1071
|
+
/** @type {window.shaka.util.EventManager.ListenerType} */
|
|
1072
|
+
event => this.onWebkitKeyAdded_(event));
|
|
1073
|
+
this.eventManager_.listen(session, 'webkitkeyerror',
|
|
1074
|
+
/** @type {window.shaka.util.EventManager.ListenerType} */
|
|
1075
|
+
event => this.onWebkitKeyError_(event));
|
|
1076
|
+
this.updateKeyStatus_('status-pending');
|
|
1077
|
+
} catch (exception) {
|
|
1078
|
+
this.generateRequestPromise_.reject(exception);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
return this.generateRequestPromise_;
|
|
1082
|
+
}
|
|
1083
|
+
/** @override */
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
load() {
|
|
1087
|
+
myLog.debug('PatchedMediaKeysApple.MediaKeySession.load');
|
|
1088
|
+
return Promise.reject(new Error('MediaKeySession.load not yet supported'));
|
|
1089
|
+
}
|
|
1090
|
+
/** @override */
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
update(response) {
|
|
1094
|
+
myLog.debug('PatchedMediaKeysApple.MediaKeySession.update');
|
|
1095
|
+
this.updatePromise_ = new PublicPromise();
|
|
1096
|
+
|
|
1097
|
+
try {
|
|
1098
|
+
// Pass through to the native session.
|
|
1099
|
+
this.nativeMediaKeySession_.update(window.shaka.util.BufferUtils.toUint8(response));
|
|
1100
|
+
} catch (exception) {
|
|
1101
|
+
this.updatePromise_.reject(exception);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
return this.updatePromise_;
|
|
1105
|
+
}
|
|
1106
|
+
/** @override */
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
close() {
|
|
1110
|
+
myLog.debug('PatchedMediaKeysApple.MediaKeySession.close');
|
|
1111
|
+
|
|
1112
|
+
try {
|
|
1113
|
+
// Pass through to the native session.
|
|
1114
|
+
this.nativeMediaKeySession_.close();
|
|
1115
|
+
this.closed.resolve();
|
|
1116
|
+
this.eventManager_.removeAll();
|
|
1117
|
+
} catch (exception) {
|
|
1118
|
+
this.closed.reject(exception);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
return this.closed;
|
|
1122
|
+
}
|
|
1123
|
+
/** @override */
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
remove() {
|
|
1127
|
+
myLog.debug('PatchedMediaKeysApple.MediaKeySession.remove');
|
|
1128
|
+
return Promise.reject(new Error('MediaKeySession.remove is only applicable for persistent licenses, ' + 'which are not supported on this platform'));
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Handler for the native keymessage event on WebKitMediaKeySession.
|
|
1132
|
+
*
|
|
1133
|
+
* @param {!MediaKeyEvent} event
|
|
1134
|
+
* @private
|
|
1135
|
+
*/
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
onWebkitKeyMessage_(event) {
|
|
1139
|
+
myLog.debug('PatchedMediaKeysApple.onWebkitKeyMessage_', event); // We can now resolve this.generateRequestPromise, which should be non-null.
|
|
1140
|
+
|
|
1141
|
+
goog.asserts.assert(this.generateRequestPromise_, 'generateRequestPromise_ should be set before now!');
|
|
1142
|
+
|
|
1143
|
+
if (this.generateRequestPromise_) {
|
|
1144
|
+
this.generateRequestPromise_.resolve();
|
|
1145
|
+
this.generateRequestPromise_ = null;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const isNew = this.keyStatuses.getStatus() == undefined;
|
|
1149
|
+
const data = new Map().set('messageType', isNew ? 'license-request' : 'license-renewal').set('message', window.shaka.util.BufferUtils.toArrayBuffer(event.message));
|
|
1150
|
+
const event2 = new window.shaka.util.FakeEvent('message', data);
|
|
1151
|
+
this.dispatchEvent(event2);
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Handler for the native keyadded event on WebKitMediaKeySession.
|
|
1155
|
+
*
|
|
1156
|
+
* @param {!MediaKeyEvent} event
|
|
1157
|
+
* @private
|
|
1158
|
+
*/
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
onWebkitKeyAdded_(event) {
|
|
1162
|
+
myLog.debug('PatchedMediaKeysApple.onWebkitKeyAdded_', event); // This shouldn't fire while we're in the middle of generateRequest,
|
|
1163
|
+
// but if it does, we will need to change the logic to account for it.
|
|
1164
|
+
|
|
1165
|
+
goog.asserts.assert(!this.generateRequestPromise_, 'Key added during generate!'); // We can now resolve this.updatePromise, which should be non-null.
|
|
1166
|
+
|
|
1167
|
+
goog.asserts.assert(this.updatePromise_, 'updatePromise_ should be set before now!');
|
|
1168
|
+
|
|
1169
|
+
if (this.updatePromise_) {
|
|
1170
|
+
this.updateKeyStatus_('usable');
|
|
1171
|
+
this.updatePromise_.resolve();
|
|
1172
|
+
this.updatePromise_ = null;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Handler for the native keyerror event on WebKitMediaKeySession.
|
|
1177
|
+
*
|
|
1178
|
+
* @param {!MediaKeyEvent} event
|
|
1179
|
+
* @private
|
|
1180
|
+
*/
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
onWebkitKeyError_(event) {
|
|
1184
|
+
myLog.debug('PatchedMediaKeysApple.onWebkitKeyError_', event);
|
|
1185
|
+
const error = new Error('EME PatchedMediaKeysApple key error');
|
|
1186
|
+
error.errorCode = this.nativeMediaKeySession_.error;
|
|
1187
|
+
|
|
1188
|
+
if (this.generateRequestPromise_ != null) {
|
|
1189
|
+
this.generateRequestPromise_.reject(error);
|
|
1190
|
+
this.generateRequestPromise_ = null;
|
|
1191
|
+
} else if (this.updatePromise_ != null) {
|
|
1192
|
+
this.updatePromise_.reject(error);
|
|
1193
|
+
this.updatePromise_ = null;
|
|
1194
|
+
} else {
|
|
1195
|
+
// Unexpected error - map native codes to standardised key statuses.
|
|
1196
|
+
// Possible values of this.nativeMediaKeySession_.error.code:
|
|
1197
|
+
// MEDIA_KEYERR_UNKNOWN = 1
|
|
1198
|
+
// MEDIA_KEYERR_CLIENT = 2
|
|
1199
|
+
// MEDIA_KEYERR_SERVICE = 3
|
|
1200
|
+
// MEDIA_KEYERR_OUTPUT = 4
|
|
1201
|
+
// MEDIA_KEYERR_HARDWARECHANGE = 5
|
|
1202
|
+
// MEDIA_KEYERR_DOMAIN = 6
|
|
1203
|
+
switch (this.nativeMediaKeySession_.error.code) {
|
|
1204
|
+
case window.WebKitMediaKeyError.MEDIA_KEYERR_OUTPUT:
|
|
1205
|
+
case window.WebKitMediaKeyError.MEDIA_KEYERR_HARDWARECHANGE:
|
|
1206
|
+
this.updateKeyStatus_('output-not-allowed');
|
|
1207
|
+
break;
|
|
1208
|
+
|
|
1209
|
+
default:
|
|
1210
|
+
this.updateKeyStatus_('internal-error');
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Updates key status and dispatch a 'keystatuseschange' event.
|
|
1217
|
+
*
|
|
1218
|
+
* @param {string} status
|
|
1219
|
+
* @private
|
|
1220
|
+
*/
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
updateKeyStatus_(status) {
|
|
1224
|
+
this.keyStatuses.setStatus(status);
|
|
1225
|
+
const event = new window.shaka.util.FakeEvent('keystatuseschange');
|
|
1226
|
+
this.dispatchEvent(event);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
const getDummyKeyId = () => new Uint8Array([0]).buffer;
|
|
1232
|
+
/**
|
|
1233
|
+
* @summary An implementation of MediaKeyStatusMap.
|
|
1234
|
+
* This fakes a map with a single key ID.
|
|
1235
|
+
*
|
|
1236
|
+
* @todo Consolidate the MediaKeyStatusMap types in these polyfills.
|
|
1237
|
+
* @implements {MediaKeyStatusMap}
|
|
1238
|
+
*/
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
PatchedMediaKeysApple.MediaKeyStatusMap = class {
|
|
1242
|
+
/** */
|
|
1243
|
+
constructor() {
|
|
1244
|
+
/**
|
|
1245
|
+
* @type {number}
|
|
1246
|
+
*/
|
|
1247
|
+
this.size = 0;
|
|
1248
|
+
/**
|
|
1249
|
+
* @private {string|undefined}
|
|
1250
|
+
*/
|
|
1251
|
+
|
|
1252
|
+
this.status_ = undefined;
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* An internal method used by the session to set key status.
|
|
1256
|
+
* @param {string|undefined} status
|
|
1257
|
+
*/
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
setStatus(status) {
|
|
1261
|
+
this.size = status == undefined ? 0 : 1;
|
|
1262
|
+
this.status_ = status;
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* An internal method used by the session to get key status.
|
|
1266
|
+
* @return {string|undefined}
|
|
1267
|
+
*/
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
getStatus() {
|
|
1271
|
+
return this.status_;
|
|
1272
|
+
}
|
|
1273
|
+
/** @override */
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
forEach(fn) {
|
|
1277
|
+
if (this.status_) {
|
|
1278
|
+
fn(this.status_, getDummyKeyId());
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
/** @override */
|
|
1282
|
+
|
|
1283
|
+
|
|
1284
|
+
get(keyId) {
|
|
1285
|
+
if (this.has(keyId)) {
|
|
1286
|
+
return this.status_;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
return undefined;
|
|
1290
|
+
}
|
|
1291
|
+
/** @override */
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
has(keyId) {
|
|
1295
|
+
const fakeKeyId = getDummyKeyId();
|
|
1296
|
+
|
|
1297
|
+
if (this.status_ && window.shaka.util.BufferUtils.equal(keyId, fakeKeyId)) {
|
|
1298
|
+
return true;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* @suppress {missingReturn}
|
|
1305
|
+
* @override
|
|
1306
|
+
*/
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
entries() {
|
|
1310
|
+
goog.asserts.assert(false, 'Not used! Provided only for the compiler.');
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* @suppress {missingReturn}
|
|
1314
|
+
* @override
|
|
1315
|
+
*/
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
keys() {
|
|
1319
|
+
goog.asserts.assert(false, 'Not used! Provided only for the compiler.');
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* @suppress {missingReturn}
|
|
1323
|
+
* @override
|
|
1324
|
+
*/
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
values() {
|
|
1328
|
+
goog.asserts.assert(false, 'Not used! Provided only for the compiler.');
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
/* eslint-disable no-param-reassign */
|
|
1334
|
+
|
|
1335
|
+
const getQualityItem = track => ({
|
|
1336
|
+
id: track.originalVideoId,
|
|
1337
|
+
bitrate: track.videoBandwidth,
|
|
1338
|
+
width: track.width,
|
|
1339
|
+
height: track.height,
|
|
1340
|
+
codec: track.videoCodec,
|
|
1341
|
+
frameRate: track.frameRate
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
const retryStatus = {
|
|
1345
|
+
lastTime: 0,
|
|
1346
|
+
attempts: 0
|
|
1347
|
+
};
|
|
1348
|
+
|
|
1349
|
+
const handleSafariNetworkError = player => {
|
|
1350
|
+
var _player$reload;
|
|
1351
|
+
|
|
1352
|
+
if (Date.now() + 30000 < retryStatus.lastTime && retryStatus.attempts >= 3) {
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
if (Date.now() + 30000 >= retryStatus.lastTime) {
|
|
1357
|
+
retryStatus.attempts = 0;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
retryStatus.lastTime = Date.now();
|
|
1361
|
+
retryStatus.attempts += 1;
|
|
1362
|
+
(_player$reload = player.reload) === null || _player$reload === void 0 ? void 0 : _player$reload.call(player);
|
|
1363
|
+
};
|
|
1364
|
+
|
|
1365
|
+
const loadShaka = async (videoElement, config = {}) => {
|
|
1366
|
+
let player;
|
|
1367
|
+
getUrlObject(mediaSource => {
|
|
1368
|
+
player.mediaSource = mediaSource;
|
|
1369
|
+
});
|
|
1370
|
+
const shaka = await import('shaka-player');
|
|
1371
|
+
window.shaka = shaka;
|
|
1372
|
+
shaka.polyfill.installAll();
|
|
1373
|
+
|
|
1374
|
+
if (window.WebKitMediaKeys) {
|
|
1375
|
+
PatchedMediaKeysApple.install(shaka);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
player = new shaka.Player(videoElement);
|
|
1379
|
+
|
|
1380
|
+
if (window.WebKitMediaKeys) {
|
|
1381
|
+
PatchedMediaKeysApple.setupPlayer(player);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
player.configure({
|
|
1385
|
+
manifest: {
|
|
1386
|
+
dash: {
|
|
1387
|
+
ignoreSuggestedPresentationDelay: true
|
|
1388
|
+
},
|
|
1389
|
+
retryParameters: {
|
|
1390
|
+
maxAttempts: 6
|
|
1391
|
+
}
|
|
1392
|
+
},
|
|
1393
|
+
streaming: {
|
|
1394
|
+
// To reduce the unseekable range at the start of the manifests.
|
|
1395
|
+
// See: https://github.com/shaka-project/shaka-player/issues/3526
|
|
1396
|
+
safeSeekOffset: 0,
|
|
1397
|
+
rebufferingGoal: 0,
|
|
1398
|
+
retryParameters: {
|
|
1399
|
+
maxAttempts: 6
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
player.configure(config);
|
|
1404
|
+
player.addEventListener('error', event => {
|
|
1405
|
+
var _window$Sentry;
|
|
1406
|
+
|
|
1407
|
+
console.log(event);
|
|
1408
|
+
const {
|
|
1409
|
+
detail = {}
|
|
1410
|
+
} = event;
|
|
1411
|
+
|
|
1412
|
+
if (isSafari() && (detail.code === 3016 || detail.code === 1002)) {
|
|
1413
|
+
return handleSafariNetworkError(player);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
const error = new Error(`Player: ${detail.code}/${detail.name}`);
|
|
1417
|
+
|
|
1418
|
+
if (!detail || /The video element has thrown a media error|Video element triggered an Error/.test(detail.message)) {
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
videoElement.dispatchEvent(Object.assign(new CustomEvent('error'), {
|
|
1423
|
+
error: detail,
|
|
1424
|
+
message: `Player Error: ${detail.code}/${detail.message.split(' ', 3)[2]}`
|
|
1425
|
+
}));
|
|
1426
|
+
(_window$Sentry = window.Sentry) === null || _window$Sentry === void 0 ? void 0 : _window$Sentry.captureException(error);
|
|
1427
|
+
});
|
|
1428
|
+
player.addEventListener('loaded', () => videoElement.dispatchEvent(new CustomEvent('canplay')));
|
|
1429
|
+
player.addEventListener('adaptation', event => {
|
|
1430
|
+
const {
|
|
1431
|
+
videoBandwidth,
|
|
1432
|
+
width,
|
|
1433
|
+
height
|
|
1434
|
+
} = event.newTrack;
|
|
1435
|
+
videoElement.dispatchEvent(new CustomEvent('downloadQualityChange', {
|
|
1436
|
+
detail: {
|
|
1437
|
+
bitrate: parseInt(videoBandwidth / 1000, 10),
|
|
1438
|
+
height,
|
|
1439
|
+
width
|
|
1440
|
+
}
|
|
1441
|
+
}));
|
|
1442
|
+
});
|
|
1443
|
+
const extensionOptions = {
|
|
1444
|
+
licenseRequestHeaders: null
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1447
|
+
const getAvailableVideoQualities = () => player.getVariantTracks().reduce((trackList, currentTrack) => {
|
|
1448
|
+
const keepOrignalTrack = trackList.find(track => track.height === currentTrack.height);
|
|
1449
|
+
|
|
1450
|
+
if (!keepOrignalTrack) {
|
|
1451
|
+
trackList.push(getQualityItem(currentTrack));
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
return trackList;
|
|
1455
|
+
}, []);
|
|
1456
|
+
|
|
1457
|
+
const getVideoQuality = () => {
|
|
1458
|
+
const activeTrack = player.getVariantTracks().find(track => track.active);
|
|
1459
|
+
if (!activeTrack) return {};
|
|
1460
|
+
return getQualityItem(activeTrack);
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
HttpFetchPlugin.register(shaka);
|
|
1464
|
+
player.getNetworkingEngine().registerRequestFilter((type, request) => {
|
|
1465
|
+
const {
|
|
1466
|
+
LICENSE,
|
|
1467
|
+
SERVER_CERTIFICATE
|
|
1468
|
+
} = shaka.net.NetworkingEngine.RequestType;
|
|
1469
|
+
|
|
1470
|
+
if (type === SERVER_CERTIFICATE) {
|
|
1471
|
+
var _extensionOptions$drm;
|
|
1472
|
+
|
|
1473
|
+
request.headers = { ...request.headers,
|
|
1474
|
+
...((_extensionOptions$drm = extensionOptions.drm[player.drmInfo().keySystem]) === null || _extensionOptions$drm === void 0 ? void 0 : _extensionOptions$drm.certificateHeaders)
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
if (type === LICENSE) {
|
|
1479
|
+
var _extensionOptions$drm2, _player$licenseReques, _player;
|
|
1480
|
+
|
|
1481
|
+
request.headers = { ...request.headers,
|
|
1482
|
+
...((_extensionOptions$drm2 = extensionOptions.drm[player.drmInfo().keySystem]) === null || _extensionOptions$drm2 === void 0 ? void 0 : _extensionOptions$drm2.headers)
|
|
1483
|
+
};
|
|
1484
|
+
(_player$licenseReques = (_player = player).licenseRequestHandler) === null || _player$licenseReques === void 0 ? void 0 : _player$licenseReques.call(_player, request);
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
const extensions = {
|
|
1488
|
+
shaka,
|
|
1489
|
+
|
|
1490
|
+
get mediaSource() {
|
|
1491
|
+
return player.mediaSource;
|
|
1492
|
+
},
|
|
1493
|
+
|
|
1494
|
+
configureExtensions: ({
|
|
1495
|
+
drm
|
|
1496
|
+
} = {}) => {
|
|
1497
|
+
extensionOptions.drm = drm;
|
|
1498
|
+
},
|
|
1499
|
+
getPlaybackSpeed: () => videoElement.playbackRate,
|
|
1500
|
+
getVideoElement: () => videoElement,
|
|
1501
|
+
setQuality: restrictions => {
|
|
1502
|
+
if (!restrictions) return; // FIXME: Setting restrictions to {} cannot enable abr.
|
|
1503
|
+
|
|
1504
|
+
player.configure('abr.restrictions', restrictions);
|
|
1505
|
+
},
|
|
1506
|
+
getVideoQuality,
|
|
1507
|
+
getAvailableVideoQualities,
|
|
1508
|
+
getSubtitles: () => player.getTextTracks().map(track => ({
|
|
1509
|
+
label: track.label,
|
|
1510
|
+
value: track.language,
|
|
1511
|
+
enabled: track.active
|
|
1512
|
+
})),
|
|
1513
|
+
setSubtitleTrack: lang => {
|
|
1514
|
+
var _player3, _player4;
|
|
1515
|
+
|
|
1516
|
+
if (lang === 'off') {
|
|
1517
|
+
var _player2;
|
|
1518
|
+
|
|
1519
|
+
(_player2 = player) === null || _player2 === void 0 ? void 0 : _player2.setTextTrackVisibility(false);
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
(_player3 = player) === null || _player3 === void 0 ? void 0 : _player3.selectTextLanguage(lang);
|
|
1524
|
+
(_player4 = player) === null || _player4 === void 0 ? void 0 : _player4.setTextTrackVisibility(true);
|
|
1525
|
+
},
|
|
1526
|
+
getAudio: () => {
|
|
1527
|
+
var _player5;
|
|
1528
|
+
|
|
1529
|
+
const active = (_player5 = player) === null || _player5 === void 0 ? void 0 : _player5.getVariantTracks().find(track => track.active);
|
|
1530
|
+
return {
|
|
1531
|
+
lang: active === null || active === void 0 ? void 0 : active.language,
|
|
1532
|
+
label: active === null || active === void 0 ? void 0 : active.label
|
|
1533
|
+
};
|
|
1534
|
+
},
|
|
1535
|
+
getAudioList: () => player.getAudioLanguages().map(lang => ({
|
|
1536
|
+
lang,
|
|
1537
|
+
label: lang
|
|
1538
|
+
})),
|
|
1539
|
+
setAudioTrack: lang => {
|
|
1540
|
+
var _player6;
|
|
1541
|
+
|
|
1542
|
+
if (!lang) return;
|
|
1543
|
+
(_player6 = player) === null || _player6 === void 0 ? void 0 : _player6.selectAudioLanguage(lang);
|
|
1544
|
+
},
|
|
1545
|
+
on: player.addEventListener.bind(player)
|
|
1546
|
+
};
|
|
1547
|
+
Object.assign(player, extensions);
|
|
1548
|
+
return player;
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
/* eslint-disable no-plusplus */
|
|
1552
|
+
new UAParser();
|
|
1553
|
+
function needNativeHls() {
|
|
1554
|
+
// Don't let Android phones play HLS, even if some of them report supported
|
|
1555
|
+
// This covers Samsung & OPPO special cases
|
|
1556
|
+
const isAndroid = /android|X11|Linux/i.test(navigator.userAgent); // canPlayType isn't reliable across all iOS verion / device combinations, so also check user agent
|
|
1557
|
+
|
|
1558
|
+
const isSafari = /^((?!chrome|android|X11|Linux).)*(safari|iPad|iPhone|Version)/i.test(navigator.userAgent);
|
|
1559
|
+
|
|
1560
|
+
if (isSafari) {
|
|
1561
|
+
return 'maybe';
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
const isFirefox = /firefox/i.test(navigator.userAgent);
|
|
1565
|
+
const isEdge = /edg/i.test(navigator.userAgent);
|
|
1566
|
+
const isChrome = /chrome/i.test(navigator.userAgent) && !isEdge;
|
|
1567
|
+
|
|
1568
|
+
if (isAndroid || isFirefox || isEdge || isChrome) {
|
|
1569
|
+
return "";
|
|
1570
|
+
} // ref: https://stackoverflow.com/a/12905122/4578017
|
|
1571
|
+
// none of our supported browsers other than Safari response to this
|
|
1572
|
+
|
|
1573
|
+
|
|
1574
|
+
return document.createElement('video').canPlayType('application/vnd.apple.mpegURL');
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/* eslint-disable no-param-reassign */
|
|
1578
|
+
const keySystems = {
|
|
1579
|
+
widevine: 'com.widevine.alpha',
|
|
1580
|
+
fairplay: 'com.apple.fps.1_0',
|
|
1581
|
+
playready: 'com.microsoft.playready'
|
|
1582
|
+
};
|
|
1583
|
+
|
|
1584
|
+
const getDrmOptions$1 = source => {
|
|
1585
|
+
const drm = source.drm && Object.entries(source.drm).reduce((result, [keySystemId, options]) => {
|
|
1586
|
+
const uri = typeof options === 'string' ? options : options.licenseUri;
|
|
1587
|
+
|
|
1588
|
+
if (uri) {
|
|
1589
|
+
const keySystemName = keySystems[keySystemId] || keySystemId;
|
|
1590
|
+
result.servers[keySystemName] = uri;
|
|
1591
|
+
|
|
1592
|
+
if (options.certificateUri) {
|
|
1593
|
+
result.advanced[keySystemName] = {
|
|
1594
|
+
serverCertificateUri: options.certificateUri
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
return result;
|
|
1600
|
+
}, {
|
|
1601
|
+
servers: {},
|
|
1602
|
+
advanced: {}
|
|
1603
|
+
});
|
|
1604
|
+
const extensions = source.drm && Object.entries(source.drm).reduce((result, [keySystemId, options]) => {
|
|
1605
|
+
const keySystemName = keySystems[keySystemId] || keySystemId;
|
|
1606
|
+
|
|
1607
|
+
if (options.headers || options.certificateHeaders) {
|
|
1608
|
+
result[keySystemName] = {
|
|
1609
|
+
headers: options.headers,
|
|
1610
|
+
...(options.certificateHeaders && {
|
|
1611
|
+
certificateHeaders: options.certificateHeaders
|
|
1612
|
+
})
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
return result;
|
|
1617
|
+
}, {});
|
|
1618
|
+
return [drm, {
|
|
1619
|
+
drm: extensions
|
|
1620
|
+
}];
|
|
1621
|
+
};
|
|
1622
|
+
|
|
1623
|
+
const FairplayKeySystem = {
|
|
1624
|
+
prepareContentId: contentUri => {
|
|
1625
|
+
const uriParts = contentUri.split('://');
|
|
1626
|
+
const contentId = uriParts[1] || '';
|
|
1627
|
+
return uriParts[0].slice(-3).toLowerCase() === 'skd' ? contentId : '';
|
|
1628
|
+
},
|
|
1629
|
+
prepareCertificate: cert => new Uint8Array(cert),
|
|
1630
|
+
prepareMessage: (keyMessageEvent, keySession) => {
|
|
1631
|
+
const spc = encodeURIComponent(keyMessageEvent.messageBase64Encoded);
|
|
1632
|
+
const assetId = encodeURIComponent(keySession.contentId);
|
|
1633
|
+
return `spc=${spc}&asset_id=${assetId}`;
|
|
1634
|
+
},
|
|
1635
|
+
prepareLicense: license => {
|
|
1636
|
+
if (license.substr(0, 5) === '<ckc>' && license.substr(-6) === '</ckc>') {
|
|
1637
|
+
return license.slice(5, -6);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
return license;
|
|
1641
|
+
}
|
|
1642
|
+
};
|
|
1643
|
+
|
|
1644
|
+
const defaultCertificateUrl = url => `${url === null || url === void 0 ? void 0 : url.replace(/\/$/, '')}/fairplay_cert`;
|
|
1645
|
+
|
|
1646
|
+
const getDrmConfig = ({
|
|
1647
|
+
url,
|
|
1648
|
+
headers,
|
|
1649
|
+
widevine = {
|
|
1650
|
+
level: undefined
|
|
1651
|
+
},
|
|
1652
|
+
fairplay = {}
|
|
1653
|
+
}) => {
|
|
1654
|
+
if (!url) {
|
|
1655
|
+
return {};
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
return {
|
|
1659
|
+
widevine: {
|
|
1660
|
+
LA_URL: url,
|
|
1661
|
+
withCredentials: false,
|
|
1662
|
+
headers,
|
|
1663
|
+
...((widevine === null || widevine === void 0 ? void 0 : widevine.level) && {
|
|
1664
|
+
videoRobustness: widevine === null || widevine === void 0 ? void 0 : widevine.level
|
|
1665
|
+
})
|
|
1666
|
+
},
|
|
1667
|
+
fairplay: {
|
|
1668
|
+
LA_URL: url,
|
|
1669
|
+
withCredentials: false,
|
|
1670
|
+
headers,
|
|
1671
|
+
certificateURL: fairplay.certificateURL || defaultCertificateUrl(url),
|
|
1672
|
+
certificateHeaders: fairplay.certificateHeaders,
|
|
1673
|
+
...FairplayKeySystem
|
|
1674
|
+
},
|
|
1675
|
+
playready: {
|
|
1676
|
+
LA_URL: url,
|
|
1677
|
+
withCredentials: false,
|
|
1678
|
+
headers
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
};
|
|
1682
|
+
|
|
1683
|
+
const loadBitmovin = async ({
|
|
1684
|
+
container,
|
|
1685
|
+
videoElement,
|
|
1686
|
+
autoplay,
|
|
1687
|
+
config = {}
|
|
1688
|
+
}) => {
|
|
1689
|
+
// Don't move module paths to array or other variables! they need to be resolved by bundlers
|
|
1690
|
+
const {
|
|
1691
|
+
Player,
|
|
1692
|
+
PlayerEvent
|
|
1693
|
+
} = await import('bitmovin-player/modules/bitmovinplayer-core');
|
|
1694
|
+
const nativeHls = needNativeHls();
|
|
1695
|
+
const bitmovinModules = [].concat(await import('bitmovin-player/modules/bitmovinplayer-engine-bitmovin'), nativeHls && (await import('bitmovin-player/modules/bitmovinplayer-engine-native')), await Promise.all([import('bitmovin-player/modules/bitmovinplayer-drm'), import('bitmovin-player/modules/bitmovinplayer-abr'), import('bitmovin-player/modules/bitmovinplayer-subtitles'), import('bitmovin-player/modules/bitmovinplayer-container-mp4')]), nativeHls && (await Promise.all([import('bitmovin-player/modules/bitmovinplayer-hls'), import('bitmovin-player/modules/bitmovinplayer-subtitles-native')])), !nativeHls && (await import('bitmovin-player/modules/bitmovinplayer-subtitles-vtt')), !nativeHls && (await import('bitmovin-player/modules/bitmovinplayer-xml')), !nativeHls && (await Promise.all([import('bitmovin-player/modules/bitmovinplayer-dash'), import('bitmovin-player/modules/bitmovinplayer-mserenderer'), import('bitmovin-player/modules/bitmovinplayer-polyfill')]))).filter(Boolean);
|
|
1696
|
+
bitmovinModules.forEach(module => Player.addModule(module.default));
|
|
1697
|
+
const extensionOptions = {
|
|
1698
|
+
drm: {}
|
|
1699
|
+
};
|
|
1700
|
+
let adaptationHandler;
|
|
1701
|
+
const player = new Player(container, {
|
|
1702
|
+
ui: false,
|
|
1703
|
+
...config,
|
|
1704
|
+
playback: { ...config.playback,
|
|
1705
|
+
autoplay: true
|
|
1706
|
+
},
|
|
1707
|
+
adaptation: { ...config.adaptation,
|
|
1708
|
+
onVideoAdaptation: data => {
|
|
1709
|
+
var _adaptationHandler;
|
|
1710
|
+
|
|
1711
|
+
const availableQualities = player.getAvailableVideoQualities();
|
|
1712
|
+
return ((_adaptationHandler = adaptationHandler) === null || _adaptationHandler === void 0 ? void 0 : _adaptationHandler({
|
|
1713
|
+
availableQualities,
|
|
1714
|
+
suggested: availableQualities.find(item => item.id === data.suggested) || {
|
|
1715
|
+
id: data.suggested
|
|
1716
|
+
}
|
|
1717
|
+
})) || data.suggested;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
player.configure = ({
|
|
1723
|
+
drm
|
|
1724
|
+
}) => {
|
|
1725
|
+
if (drm) {
|
|
1726
|
+
extensionOptions.drm = drm;
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
|
|
1730
|
+
player.configureExtensions = ({
|
|
1731
|
+
drm
|
|
1732
|
+
} = {}) => {
|
|
1733
|
+
if (drm) {
|
|
1734
|
+
var _Object$values$;
|
|
1735
|
+
|
|
1736
|
+
extensionOptions.licenseRequestHeaders = (_Object$values$ = Object.values(drm)[0]) === null || _Object$values$ === void 0 ? void 0 : _Object$values$.headers;
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
|
|
1740
|
+
const originalLoad = player.load;
|
|
1741
|
+
|
|
1742
|
+
player.load = (src, startTime, type) => {
|
|
1743
|
+
const {
|
|
1744
|
+
muted
|
|
1745
|
+
} = videoElement;
|
|
1746
|
+
originalLoad.call(player, {
|
|
1747
|
+
[type === 'application/x-mpegurl' ? 'hls' : 'dash']: src,
|
|
1748
|
+
drm: getDrmConfig({
|
|
1749
|
+
url: ['com.apple.fps.1_0', 'com.widevine.alpha', 'com.microsoft.playready'].map(keySystemName => {
|
|
1750
|
+
var _extensionOptions$drm, _extensionOptions$drm2;
|
|
1751
|
+
|
|
1752
|
+
return (_extensionOptions$drm = extensionOptions.drm) === null || _extensionOptions$drm === void 0 ? void 0 : (_extensionOptions$drm2 = _extensionOptions$drm.servers) === null || _extensionOptions$drm2 === void 0 ? void 0 : _extensionOptions$drm2[keySystemName];
|
|
1753
|
+
}).find(Boolean),
|
|
1754
|
+
headers: extensionOptions.licenseRequestHeaders
|
|
1755
|
+
}),
|
|
1756
|
+
...(startTime && {
|
|
1757
|
+
options: {
|
|
1758
|
+
startTime
|
|
1759
|
+
}
|
|
1760
|
+
})
|
|
1761
|
+
}).then(result => {
|
|
1762
|
+
// Bitmovin resets muted state after load in Safari, so restore it
|
|
1763
|
+
if (muted) {
|
|
1764
|
+
player.mute();
|
|
1765
|
+
} else {
|
|
1766
|
+
player.unmute();
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
return result;
|
|
1770
|
+
});
|
|
1771
|
+
};
|
|
1772
|
+
|
|
1773
|
+
player.setAdaptationHandler = handler => {
|
|
1774
|
+
adaptationHandler = handler;
|
|
1775
|
+
};
|
|
1776
|
+
|
|
1777
|
+
player.seekRange = player.getSeekableRange;
|
|
1778
|
+
player.getDrmConfig = getDrmConfig; // Mock Shaka player interface from shaka.js
|
|
1779
|
+
|
|
1780
|
+
player.getSubtitles = () => {
|
|
1781
|
+
var _player$subtitles;
|
|
1782
|
+
|
|
1783
|
+
return ((_player$subtitles = player.subtitles) === null || _player$subtitles === void 0 ? void 0 : _player$subtitles.list().map(track => ({
|
|
1784
|
+
label: track.label,
|
|
1785
|
+
value: track.lang,
|
|
1786
|
+
enabled: track.enabled
|
|
1787
|
+
}))) || [];
|
|
1788
|
+
};
|
|
1789
|
+
|
|
1790
|
+
player.setSubtitleTrack = language => {
|
|
1791
|
+
var _subtitles$list;
|
|
1792
|
+
|
|
1793
|
+
const {
|
|
1794
|
+
subtitles
|
|
1795
|
+
} = player;
|
|
1796
|
+
subtitles === null || subtitles === void 0 ? void 0 : (_subtitles$list = subtitles.list) === null || _subtitles$list === void 0 ? void 0 : _subtitles$list.call(subtitles).forEach(track => {
|
|
1797
|
+
// TODO consider multiple subtitles
|
|
1798
|
+
subtitles[language === track.lang ? 'enable' : 'disable'](track.id); // Safari need to fire cueExit manually.
|
|
1799
|
+
|
|
1800
|
+
if (language === 'off') subtitles.cueExit();
|
|
1801
|
+
});
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1804
|
+
player.getAudioList = () => player.getAvailableAudio();
|
|
1805
|
+
|
|
1806
|
+
player.setAudioTrack = language => {
|
|
1807
|
+
const track = player.getAvailableAudio().find(audio => audio.lang === language);
|
|
1808
|
+
|
|
1809
|
+
if (track) {
|
|
1810
|
+
player.setAudio(track.id);
|
|
1811
|
+
}
|
|
1812
|
+
};
|
|
1813
|
+
|
|
1814
|
+
player.setVideoElement(videoElement); // For a paused live stream, Bitmovin constantly download latest segments and update,
|
|
1815
|
+
// and may unexpectedly resume playing when playing vod-to-live, so set speed 0 to prevent.
|
|
1816
|
+
// #CPT-1783
|
|
1817
|
+
|
|
1818
|
+
player.on(PlayerEvent.Play, () => {
|
|
1819
|
+
if (player.isLive()) {
|
|
1820
|
+
player.setPlaybackSpeed(1);
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1823
|
+
player.on(PlayerEvent.Paused, () => {
|
|
1824
|
+
if (player.isLive()) {
|
|
1825
|
+
player.setPlaybackSpeed(0);
|
|
1826
|
+
}
|
|
1827
|
+
});
|
|
1828
|
+
player.on(PlayerEvent.SourceLoaded, () => {
|
|
1829
|
+
if (player.isLive()) {
|
|
1830
|
+
// eslint-disable-next-line no-param-reassign
|
|
1831
|
+
player.setPlaybackSpeed(1); // no video event fires when live stream loaded, fire one so that we can handle like VOD
|
|
1832
|
+
|
|
1833
|
+
videoElement.dispatchEvent(new Event('canplay'));
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
player.on(PlayerEvent.Error, info => {
|
|
1837
|
+
var _window$Sentry;
|
|
1838
|
+
|
|
1839
|
+
const error = new Error(`Player: ${info.code}/${info.name}`);
|
|
1840
|
+
console.warn(info);
|
|
1841
|
+
|
|
1842
|
+
if (/The video element has thrown a media error|Video element triggered an Error/.test(info.message)) {
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
(_window$Sentry = window.Sentry) === null || _window$Sentry === void 0 ? void 0 : _window$Sentry.captureException(error);
|
|
1847
|
+
videoElement.dispatchEvent(Object.assign(new CustomEvent('error'), {
|
|
1848
|
+
error: info,
|
|
1849
|
+
message: `Player Error: ${info.code}/${info.name}`
|
|
1850
|
+
}));
|
|
1851
|
+
});
|
|
1852
|
+
player.on(PlayerEvent.StallStarted, () => videoElement.dispatchEvent(new Event('waiting')));
|
|
1853
|
+
player.on(PlayerEvent.VideoDownloadQualityChanged, event => videoElement.dispatchEvent(new CustomEvent('downloadQualityChange', {
|
|
1854
|
+
detail: {
|
|
1855
|
+
bitrate: parseInt(event.targetQuality.bitrate / 1000, 10),
|
|
1856
|
+
height: event.targetQuality.height,
|
|
1857
|
+
width: event.targetQuality.width
|
|
1858
|
+
}
|
|
1859
|
+
})));
|
|
1860
|
+
return player;
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1863
|
+
const loadPlayer = async (videoElement, {
|
|
1864
|
+
container,
|
|
1865
|
+
autoplay,
|
|
1866
|
+
source,
|
|
1867
|
+
shaka,
|
|
1868
|
+
bitmovin
|
|
1869
|
+
}) => {
|
|
1870
|
+
if (source !== null && source !== void 0 && source.native) {
|
|
1871
|
+
const player = await loadNative({
|
|
1872
|
+
videoElement
|
|
1873
|
+
});
|
|
1874
|
+
return player;
|
|
1875
|
+
} // default to Shaka
|
|
1876
|
+
|
|
1877
|
+
|
|
1878
|
+
if (shaka || !bitmovin) {
|
|
1879
|
+
const player = await loadShaka(videoElement, shaka);
|
|
1880
|
+
|
|
1881
|
+
if (autoplay) {
|
|
1882
|
+
// eslint-disable-next-line no-param-reassign
|
|
1883
|
+
videoElement.autoplay = true;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
videoElement.dispatchEvent(new CustomEvent('playerStarted'));
|
|
1887
|
+
return player;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
if (bitmovin) {
|
|
1891
|
+
const player = await loadBitmovin({
|
|
1892
|
+
container,
|
|
1893
|
+
videoElement,
|
|
1894
|
+
autoplay,
|
|
1895
|
+
config: bitmovin
|
|
1896
|
+
});
|
|
1897
|
+
videoElement.dispatchEvent(new CustomEvent('playerStarted'));
|
|
1898
|
+
return player;
|
|
1899
|
+
} // TODO load other players: dash.js, hls.js
|
|
1900
|
+
|
|
1901
|
+
};
|
|
1902
|
+
|
|
1903
|
+
const once = (target, name, handler) => {
|
|
1904
|
+
const oneTime = (...args) => {
|
|
1905
|
+
handler(...args);
|
|
1906
|
+
target.removeEventListener(name, oneTime);
|
|
1907
|
+
};
|
|
1908
|
+
|
|
1909
|
+
target.addEventListener(name, oneTime);
|
|
1910
|
+
return () => target.removeEventListener(name, oneTime);
|
|
1911
|
+
};
|
|
1912
|
+
|
|
1913
|
+
/* eslint-disable no-param-reassign */
|
|
1914
|
+
const VideoSourceTypeMap = {
|
|
1915
|
+
'application/dash+xml': {
|
|
1916
|
+
sourceKeyName: 'dash',
|
|
1917
|
+
extension: 'mpd'
|
|
1918
|
+
},
|
|
1919
|
+
'application/x-mpegurl': {
|
|
1920
|
+
sourceKeyName: 'hls',
|
|
1921
|
+
extension: 'm3u8'
|
|
1922
|
+
}
|
|
1923
|
+
};
|
|
1924
|
+
const mimeTypes = {
|
|
1925
|
+
hls: 'application/x-mpegurl',
|
|
1926
|
+
dash: 'application/dash+xml'
|
|
1927
|
+
};
|
|
1928
|
+
|
|
1929
|
+
const getExtensionByType = srcType => {
|
|
1930
|
+
var _VideoSourceTypeMap$s;
|
|
1931
|
+
|
|
1932
|
+
return (_VideoSourceTypeMap$s = VideoSourceTypeMap[srcType]) === null || _VideoSourceTypeMap$s === void 0 ? void 0 : _VideoSourceTypeMap$s.extension;
|
|
1933
|
+
};
|
|
1934
|
+
|
|
1935
|
+
const isStringSourceWithProperExtension = (url, srcType) => {
|
|
1936
|
+
if (typeof url === 'string') {
|
|
1937
|
+
const extension = url.split('.').at(-1); // eslint-disable-next-line eqeqeq
|
|
1938
|
+
|
|
1939
|
+
if (extension == getExtensionByType(srcType)) return true;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
return false;
|
|
1943
|
+
};
|
|
1944
|
+
|
|
1945
|
+
const matchType = (source, manifestType) => {
|
|
1946
|
+
var _source$type, _source$type2;
|
|
1947
|
+
|
|
1948
|
+
return ((_source$type = source.type) === null || _source$type === void 0 ? void 0 : _source$type.includes(manifestType)) || ((_source$type2 = source.type) === null || _source$type2 === void 0 ? void 0 : _source$type2.toLowerCase()) === mimeTypes[manifestType] || isStringSourceWithProperExtension(source.src || source, manifestType);
|
|
1949
|
+
};
|
|
1950
|
+
|
|
1951
|
+
const getDrmOptions = fallbackDrm => {
|
|
1952
|
+
if (!(fallbackDrm !== null && fallbackDrm !== void 0 && fallbackDrm.url)) {
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
const drmOptions = {
|
|
1957
|
+
licenseUri: fallbackDrm.url,
|
|
1958
|
+
headers: fallbackDrm.headers
|
|
1959
|
+
};
|
|
1960
|
+
return {
|
|
1961
|
+
widevine: drmOptions,
|
|
1962
|
+
fairplay: { ...drmOptions,
|
|
1963
|
+
certificateUri: `${fallbackDrm.url}/fairplay_cert`,
|
|
1964
|
+
...fallbackDrm.fairplay
|
|
1965
|
+
},
|
|
1966
|
+
playready: drmOptions
|
|
1967
|
+
};
|
|
1968
|
+
};
|
|
1969
|
+
/**
|
|
1970
|
+
* @typedef {{src: string, type: string}} SourceObject
|
|
1971
|
+
* @typedef {{hls: string, dash: string}} SourceObjectAlt backward compatiable form
|
|
1972
|
+
*
|
|
1973
|
+
* @param {SourceObject[]|SourceObject|SourceObjectAlt|string} sourceOptions
|
|
1974
|
+
* @param {{preferManifestType?: ('dash'|'hls')}} options
|
|
1975
|
+
* @return {{src: string, type: string, drm: Object}}
|
|
1976
|
+
*/
|
|
1977
|
+
|
|
1978
|
+
|
|
1979
|
+
const getSource = (sourceOptions, {
|
|
1980
|
+
preferManifestType,
|
|
1981
|
+
fallbackDrm
|
|
1982
|
+
} = {}) => {
|
|
1983
|
+
if (sourceOptions.dash || sourceOptions.hls) {
|
|
1984
|
+
const {
|
|
1985
|
+
dash,
|
|
1986
|
+
hls
|
|
1987
|
+
} = sourceOptions;
|
|
1988
|
+
return getSource([hls && {
|
|
1989
|
+
src: hls,
|
|
1990
|
+
type: mimeTypes.hls
|
|
1991
|
+
}, dash && {
|
|
1992
|
+
src: dash,
|
|
1993
|
+
type: mimeTypes.dash
|
|
1994
|
+
}].filter(Boolean), {
|
|
1995
|
+
preferManifestType,
|
|
1996
|
+
fallbackDrm
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
if (!Array.isArray(sourceOptions)) {
|
|
2001
|
+
return getSource([sourceOptions], {
|
|
2002
|
+
preferManifestType,
|
|
2003
|
+
fallbackDrm
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
if (fallbackDrm) {
|
|
2008
|
+
return getSource(sourceOptions.map(option => ({ ...(option.src ? option : {
|
|
2009
|
+
src: option
|
|
2010
|
+
}),
|
|
2011
|
+
drm: getDrmOptions(fallbackDrm)
|
|
2012
|
+
})), {
|
|
2013
|
+
preferManifestType
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
const matched = sourceOptions.find(source => !preferManifestType || matchType(source, preferManifestType));
|
|
2018
|
+
const selected = matched || sourceOptions[0];
|
|
2019
|
+
|
|
2020
|
+
if (!selected) {
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
const type = matched && preferManifestType === 'hls' && mimeTypes.hls;
|
|
2025
|
+
return { ...(selected.src ? selected : {
|
|
2026
|
+
src: selected
|
|
2027
|
+
}),
|
|
2028
|
+
type
|
|
2029
|
+
};
|
|
2030
|
+
};
|
|
2031
|
+
|
|
2032
|
+
const timeoutError = () => new Error('request timeout');
|
|
2033
|
+
/**
|
|
2034
|
+
* @param {URL|RequestInfo} url
|
|
2035
|
+
* @param {RequestInit} options
|
|
2036
|
+
* @param {{responseType: 'json'|'text'}}
|
|
2037
|
+
*/
|
|
2038
|
+
|
|
2039
|
+
|
|
2040
|
+
const retryRequest = (url, options = {}, {
|
|
2041
|
+
responseType = 'json',
|
|
2042
|
+
timeout = 6,
|
|
2043
|
+
retryTimes = 6
|
|
2044
|
+
} = {}) => new Promise((resolve, reject) => {
|
|
2045
|
+
setTimeout(() => reject(timeoutError()), timeout * 1000);
|
|
2046
|
+
fetch(url, options).then(response => {
|
|
2047
|
+
var _response$responseTyp;
|
|
2048
|
+
|
|
2049
|
+
return resolve(((_response$responseTyp = response[responseType]) === null || _response$responseTyp === void 0 ? void 0 : _response$responseTyp.call(response)) || response);
|
|
2050
|
+
}).catch(reject);
|
|
2051
|
+
}).catch(error => {
|
|
2052
|
+
console.log(error);
|
|
2053
|
+
|
|
2054
|
+
if (retryTimes > 0) {
|
|
2055
|
+
return retryRequest(url, options, {
|
|
2056
|
+
timeout,
|
|
2057
|
+
retryTimes: retryTimes - 1
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
return error;
|
|
2062
|
+
});
|
|
2063
|
+
|
|
2064
|
+
const matchAll = (input, pattern) => {
|
|
2065
|
+
const flags = [pattern.global && 'g', pattern.ignoreCase && 'i', pattern.multiline && 'm'].filter(Boolean).join('');
|
|
2066
|
+
const clone = new RegExp(pattern, flags);
|
|
2067
|
+
return Array.from(function* () {
|
|
2068
|
+
let matched = true;
|
|
2069
|
+
|
|
2070
|
+
while (1) {
|
|
2071
|
+
matched = clone.exec(input);
|
|
2072
|
+
|
|
2073
|
+
if (!matched) {
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
yield matched;
|
|
2078
|
+
}
|
|
2079
|
+
}());
|
|
2080
|
+
};
|
|
2081
|
+
|
|
2082
|
+
const getHlsQualityOptions = async hlsUrl => {
|
|
2083
|
+
const manifest = await retryRequest(hlsUrl, {}, {
|
|
2084
|
+
responseType: 'text'
|
|
2085
|
+
});
|
|
2086
|
+
const resolutionList = matchAll(manifest, /RESOLUTION=\d+x(\d+)/g);
|
|
2087
|
+
return Array.from(new Set(resolutionList.map(([, height]) => ({
|
|
2088
|
+
height: +height
|
|
2089
|
+
})))).sort((a, b) => b.height - a.height);
|
|
2090
|
+
};
|
|
2091
|
+
// for unit test
|
|
2092
|
+
|
|
2093
|
+
/* eslint-disable no-param-reassign */
|
|
2094
|
+
|
|
2095
|
+
const SHAKA_LIVE_DURATION = 4294967296;
|
|
2096
|
+
|
|
2097
|
+
const isLiveDuration = duration => duration >= SHAKA_LIVE_DURATION; // There's always a gap between playback head & live edge,
|
|
2098
|
+
// when the gap is small enough, we consider it is on edge.
|
|
2099
|
+
// The magic number 10s comes from observation of YouTube
|
|
2100
|
+
|
|
2101
|
+
|
|
2102
|
+
const LIVE_EDGE_GAP = 10;
|
|
2103
|
+
|
|
2104
|
+
const getLiveTime = (media, {
|
|
2105
|
+
player
|
|
2106
|
+
}) => {
|
|
2107
|
+
var _player$seekRange;
|
|
2108
|
+
|
|
2109
|
+
const now = Date.now() / 1000;
|
|
2110
|
+
const currentOffset = media.currentTime - media.defaultLiveOffset - now;
|
|
2111
|
+
const {
|
|
2112
|
+
start,
|
|
2113
|
+
end
|
|
2114
|
+
} = (player === null || player === void 0 ? void 0 : (_player$seekRange = player.seekRange) === null || _player$seekRange === void 0 ? void 0 : _player$seekRange.call(player)) || {};
|
|
2115
|
+
const seekDuration = Math.min(end - start - LIVE_EDGE_GAP / 2, media.seekDurationDiff + now);
|
|
2116
|
+
return {
|
|
2117
|
+
streamType: 'live',
|
|
2118
|
+
startTime: -seekDuration,
|
|
2119
|
+
currentTime: currentOffset < -LIVE_EDGE_GAP ? currentOffset : 0,
|
|
2120
|
+
duration: seekDuration > 5 * LIVE_EDGE_GAP ? seekDuration : 0
|
|
2121
|
+
};
|
|
2122
|
+
};
|
|
2123
|
+
|
|
2124
|
+
const getMediaTime = (media, plugins = [], player = {}) => {
|
|
2125
|
+
const {
|
|
2126
|
+
duration,
|
|
2127
|
+
...data
|
|
2128
|
+
} = Object.assign(isLiveDuration(media.initialDuration) ? getLiveTime(media, {
|
|
2129
|
+
player,
|
|
2130
|
+
plugins
|
|
2131
|
+
}) : {
|
|
2132
|
+
currentTime: media.currentTime,
|
|
2133
|
+
bufferTime: Math.max(...Array.from({
|
|
2134
|
+
length: media.buffered.length
|
|
2135
|
+
}, (_, index) => media.buffered.end(index))),
|
|
2136
|
+
duration: media.initialDuration // monkey patched, duration may change for DASH playback
|
|
2137
|
+
|
|
2138
|
+
}, ...plugins.map(plugin => {
|
|
2139
|
+
var _plugin$getPlaybackSt2;
|
|
2140
|
+
|
|
2141
|
+
return (_plugin$getPlaybackSt2 = plugin.getPlaybackStatus) === null || _plugin$getPlaybackSt2 === void 0 ? void 0 : _plugin$getPlaybackSt2.call(plugin);
|
|
2142
|
+
}));
|
|
2143
|
+
return { ...data,
|
|
2144
|
+
...((isLiveDuration(media.initialDuration) || Math.abs(media.duration - media.initialDuration) < 0.5) && {
|
|
2145
|
+
duration
|
|
2146
|
+
})
|
|
2147
|
+
};
|
|
2148
|
+
};
|
|
2149
|
+
|
|
2150
|
+
const HAVE_METADATA = 1;
|
|
2151
|
+
|
|
2152
|
+
const tryPatchHlsVideoQualities = async (player, hlsUrl) => {
|
|
2153
|
+
if (/(^data)|(mp4$)/.test(hlsUrl)) {
|
|
2154
|
+
return;
|
|
2155
|
+
} // filtered manifest comes with data URI and should be ignored
|
|
2156
|
+
|
|
2157
|
+
|
|
2158
|
+
const videoQualities = await getHlsQualityOptions(hlsUrl).catch(e => {
|
|
2159
|
+
console.warn('Failed to get HLS video qualities', e);
|
|
2160
|
+
});
|
|
2161
|
+
|
|
2162
|
+
if (videoQualities) {
|
|
2163
|
+
player.getAvailableVideoQualities = () => videoQualities;
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2166
|
+
|
|
2167
|
+
const load = async (media, {
|
|
2168
|
+
player,
|
|
2169
|
+
startTime,
|
|
2170
|
+
plugins = [],
|
|
2171
|
+
drm
|
|
2172
|
+
}, source) => {
|
|
2173
|
+
const preferManifestType = needNativeHls() ? 'hls' : 'dash';
|
|
2174
|
+
const preferred = getSource(source, {
|
|
2175
|
+
preferManifestType,
|
|
2176
|
+
fallbackDrm: drm
|
|
2177
|
+
}); // There's no use case that changing DRM options without changing manifest URL, just skip
|
|
2178
|
+
|
|
2179
|
+
if (player.lastSrc === (preferred === null || preferred === void 0 ? void 0 : preferred.src)) {
|
|
2180
|
+
console.info('src is unchanged, skip load', preferred.src);
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
player.lastSrc = preferred === null || preferred === void 0 ? void 0 : preferred.src; // playlog v2 depends on this event
|
|
2185
|
+
|
|
2186
|
+
media.dispatchEvent(new Event('loadstart'));
|
|
2187
|
+
const merged = await plugins.reduce(async (loadChain, plugin) => {
|
|
2188
|
+
var _plugin$load;
|
|
2189
|
+
|
|
2190
|
+
const currentSource = await loadChain;
|
|
2191
|
+
const overrides = await ((_plugin$load = plugin.load) === null || _plugin$load === void 0 ? void 0 : _plugin$load.call(plugin, currentSource, {
|
|
2192
|
+
video: media,
|
|
2193
|
+
player,
|
|
2194
|
+
source: currentSource,
|
|
2195
|
+
startTime,
|
|
2196
|
+
streamFormat: preferManifestType,
|
|
2197
|
+
reload: async () => {
|
|
2198
|
+
// Bitmovin unexpectedly restores muted state, so save to restore
|
|
2199
|
+
const restoreMuted = player.isMuted && {
|
|
2200
|
+
muted: player.isMuted()
|
|
2201
|
+
};
|
|
2202
|
+
player.lastSrc = '';
|
|
2203
|
+
await load(media, {
|
|
2204
|
+
player,
|
|
2205
|
+
startTime,
|
|
2206
|
+
plugins,
|
|
2207
|
+
drm
|
|
2208
|
+
}, source);
|
|
2209
|
+
|
|
2210
|
+
if (restoreMuted) {
|
|
2211
|
+
player[restoreMuted.muted ? 'mute' : 'unmute']();
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
}));
|
|
2215
|
+
return overrides ? { ...currentSource,
|
|
2216
|
+
...(overrides.url && {
|
|
2217
|
+
src: overrides.url
|
|
2218
|
+
}),
|
|
2219
|
+
...(overrides.startTime >= 0 && {
|
|
2220
|
+
startTime: overrides.startTime
|
|
2221
|
+
})
|
|
2222
|
+
} : currentSource;
|
|
2223
|
+
}, { ...preferred,
|
|
2224
|
+
startTime
|
|
2225
|
+
});
|
|
2226
|
+
media.addEventListener('durationchange', () => {
|
|
2227
|
+
// media duration may change when playing VOD to live or SSAI streams, save it here for convenience
|
|
2228
|
+
media.initialDuration = media.duration;
|
|
2229
|
+
}, {
|
|
2230
|
+
once: true
|
|
2231
|
+
});
|
|
2232
|
+
media.seekDurationDiff = -Infinity;
|
|
2233
|
+
once(media, 'loadeddata', () => {
|
|
2234
|
+
const seekToStart = (delay = 1) => {
|
|
2235
|
+
if (merged.startTime > 0 || merged.startTime < 0) {
|
|
2236
|
+
// Safari may glitch if seek immediately, so wait a little bit
|
|
2237
|
+
setTimeout(() => seek(media, {
|
|
2238
|
+
player,
|
|
2239
|
+
plugins
|
|
2240
|
+
}, merged.startTime), delay);
|
|
2241
|
+
}
|
|
2242
|
+
};
|
|
2243
|
+
|
|
2244
|
+
if (player.isLive()) {
|
|
2245
|
+
player.shouldPlayFromEdge = player.isLive() && !(merged.startTime < 0);
|
|
2246
|
+
once(media, 'timeupdate', () => {
|
|
2247
|
+
player.shouldPlayFromEdge = false;
|
|
2248
|
+
const {
|
|
2249
|
+
start,
|
|
2250
|
+
end
|
|
2251
|
+
} = player.seekRange();
|
|
2252
|
+
media.defaultLiveOffset = media.currentTime - Date.now() / 1000;
|
|
2253
|
+
media.seekDurationDiff = end - start - Date.now() / 1000 - LIVE_EDGE_GAP / 2;
|
|
2254
|
+
seekToStart();
|
|
2255
|
+
});
|
|
2256
|
+
} else {
|
|
2257
|
+
seekToStart();
|
|
2258
|
+
}
|
|
2259
|
+
});
|
|
2260
|
+
const [drmOptions, extensions] = getDrmOptions$1(preferred);
|
|
2261
|
+
player.configure({
|
|
2262
|
+
drm: drmOptions
|
|
2263
|
+
});
|
|
2264
|
+
player.configureExtensions(extensions);
|
|
2265
|
+
let loadStartTime;
|
|
2266
|
+
|
|
2267
|
+
if (merged.type !== 'application/x-mpegurl') {
|
|
2268
|
+
loadStartTime = merged.startTime;
|
|
2269
|
+
} else if (merged.startTime > 0) {
|
|
2270
|
+
once(media, 'loadeddata', event => {
|
|
2271
|
+
event.preventDefault();
|
|
2272
|
+
setTimeout(() => {
|
|
2273
|
+
media.currentTime = merged.startTime;
|
|
2274
|
+
}, 66);
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
if (merged.type === 'application/x-mpegurl') {
|
|
2279
|
+
await tryPatchHlsVideoQualities(player, merged.src);
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
player.lastSeek = 0;
|
|
2283
|
+
|
|
2284
|
+
player.reload = () => player.unload().then(() => player.load(merged.src, loadStartTime, merged.type));
|
|
2285
|
+
|
|
2286
|
+
return player.unload().then(() => player.load(merged.src, loadStartTime, merged.type)).catch(error => {
|
|
2287
|
+
media.dispatchEvent(Object.assign(new CustomEvent('error'), {
|
|
2288
|
+
error
|
|
2289
|
+
}));
|
|
2290
|
+
});
|
|
2291
|
+
};
|
|
2292
|
+
|
|
2293
|
+
const seek = async (media, {
|
|
2294
|
+
player,
|
|
2295
|
+
plugins = []
|
|
2296
|
+
}, time, issuer) => {
|
|
2297
|
+
if (media.readyState < HAVE_METADATA) {
|
|
2298
|
+
await new Promise(resolve => {
|
|
2299
|
+
media.addEventListener('loadeddata', resolve, {
|
|
2300
|
+
once: true
|
|
2301
|
+
});
|
|
2302
|
+
});
|
|
2303
|
+
} // TODO skip seeking to too near point, consider SSAI cases
|
|
2304
|
+
|
|
2305
|
+
|
|
2306
|
+
const seekPlugin = plugins.find(plugin => typeof plugin.handleSeek === 'function' && plugin.isActive());
|
|
2307
|
+
|
|
2308
|
+
const seekInternal = seekTime => {
|
|
2309
|
+
var _player$isLive, _player$seek;
|
|
2310
|
+
|
|
2311
|
+
// seeking to end video may cause Shaka glich, so move back a little
|
|
2312
|
+
if (seekTime <= media.duration + 7 && seekTime >= media.duration - 1.0) {
|
|
2313
|
+
return seekInternal(media.duration - 1.1);
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
player.shouldPlayFromEdge = false;
|
|
2317
|
+
const seekOrigin = (_player$isLive = player.isLive) !== null && _player$isLive !== void 0 && _player$isLive.call(player) ? media.defaultLiveOffset + Date.now() / 1000 : 0; // when playing in Bitmovin, must call player.seek to sync internal time
|
|
2318
|
+
|
|
2319
|
+
(_player$seek = player.seek) === null || _player$seek === void 0 ? void 0 : _player$seek.call(player, seekTime, issuer); // player.seek sets time after adding segments,
|
|
2320
|
+
// set again to reflect instantly
|
|
2321
|
+
|
|
2322
|
+
if (Math.abs(seekTime) <= LIVE_EDGE_GAP && seekOrigin !== 0) {
|
|
2323
|
+
player.lastSeek = 0;
|
|
2324
|
+
player.goToLive();
|
|
2325
|
+
} else {
|
|
2326
|
+
player.lastSeek = seekTime;
|
|
2327
|
+
media.currentTime = seekTime + seekOrigin;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
once(media, 'seeked', () => {
|
|
2331
|
+
// when seeking to the end it may result in a few seconds earlier
|
|
2332
|
+
if (Math.abs(seekTime + seekOrigin - media.currentTime) > 0.5) {
|
|
2333
|
+
media.currentTime = seekTime + seekOrigin;
|
|
2334
|
+
}
|
|
2335
|
+
});
|
|
2336
|
+
};
|
|
2337
|
+
|
|
2338
|
+
if (seekPlugin) {
|
|
2339
|
+
seekPlugin.handleSeek(time, seekInternal);
|
|
2340
|
+
} else {
|
|
2341
|
+
seekInternal(time);
|
|
2342
|
+
}
|
|
2343
|
+
};
|
|
2344
|
+
|
|
2345
|
+
export { getMediaTime, load, loadPlayer, seek };
|