@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/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 };