@kkcompany/player 2.25.0-canary.24 → 2.25.0-canary.26

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/dist/core.mjs DELETED
@@ -1,3075 +0,0 @@
1
- import UAParser from 'ua-parser-js';
2
-
3
- /* eslint-disable no-plusplus */
4
- new UAParser();
5
-
6
- const isSafari = () => /^((?!chrome|android|X11|Linux).)*(safari|iPad|iPhone|Version)/i.test(navigator.userAgent);
7
- // navigator.maxTouchPoints() is not supported in Safari 11, iOS Safari 11.0-11.2 compat/compat
8
-
9
-
10
- const isIOS = () => /iPad|iPhone|iPod/.test(navigator.platform) || navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;
11
-
12
- const on = (target, name, handler, ...rest) => {
13
- target.addEventListener(name, handler, ...rest);
14
- return () => target.removeEventListener(name, handler, ...rest);
15
- };
16
-
17
- const once = (target, name, handler) => {
18
- const oneTime = (...args) => {
19
- handler(...args);
20
- target.removeEventListener(name, oneTime);
21
- };
22
-
23
- target.addEventListener(name, oneTime);
24
- return () => target.removeEventListener(name, oneTime);
25
- };
26
-
27
- const waitFor = (check, handler) => {
28
- const checkInterval = setInterval(() => {
29
- if (check()) {
30
- clearInterval(checkInterval);
31
- handler();
32
- }
33
- }, 50);
34
- return () => clearInterval(checkInterval);
35
- };
36
-
37
- function getVersion() {
38
- try {
39
- // eslint-disable-next-line no-undef
40
- return "2.25.0-canary.24";
41
- } catch (e) {
42
- return undefined;
43
- }
44
- }
45
-
46
- const printVersion = () => {
47
- console.log([`%KKCompany Web Player SDK\n-----------\nVersion: ${getVersion()}`, `We are hiring, and looking for geeks like you! please join us at https://www.kkcompany.com/zh-tw/career/`].join('\n'), 'color: #0E78F4');
48
- };
49
-
50
- /* eslint-disable no-param-reassign */
51
- const loadNative = ({
52
- videoElement
53
- }) => ({
54
- load: ({
55
- native: url
56
- }) => {
57
- videoElement.src = url;
58
- videoElement.style.height = '100%';
59
- videoElement.style.width = '100%';
60
- },
61
- play: () => videoElement.play(),
62
- pause: () => videoElement.pause(),
63
- seek: time => {
64
- videoElement.currentTime = time;
65
- },
66
- getVideoElement: () => videoElement,
67
- getVideoQuality: () => ({}),
68
- destroy: () => {}
69
- });
70
-
71
- /*
72
- We overwrite standard function for getting mediaSource object
73
- because Chrome supports VideoTrack only in experiment mode.
74
- */
75
- const getUrlObject = fn => {
76
- const createObjectURL = window.URL.createObjectURL.bind();
77
-
78
- window.URL.createObjectURL = blob => {
79
- if (blob.addSourceBuffer) {
80
- fn(blob);
81
- }
82
-
83
- return createObjectURL(blob);
84
- };
85
- };
86
-
87
- /*! @license
88
- * Shaka Player
89
- * Copyright 2016 Google LLC
90
- * SPDX-License-Identifier: Apache-2.0
91
- */
92
- let shaka$1;
93
- const shakaLog$1 = {
94
- v1: () => {}
95
- };
96
-
97
- const asMap = object => {
98
- const map = new Map();
99
-
100
- for (const key of Object.keys(object)) {
101
- map.set(key, object[key]);
102
- }
103
-
104
- return map;
105
- };
106
-
107
- const makeResponse = (headers, data, status, uri, responseURL, requestType) => {
108
- if (status >= 200 && status <= 299 && status != 202) {
109
- // Most 2xx HTTP codes are success cases.
110
-
111
- /** @type {shaka.extern.Response} */
112
- const response = {
113
- uri: responseURL || uri,
114
- originalUri: uri,
115
- data,
116
- status,
117
- headers,
118
- fromCache: !!headers['x-shaka-from-cache']
119
- };
120
- return response;
121
- }
122
-
123
- let responseText = null;
124
-
125
- try {
126
- responseText = shaka$1.util.StringUtils.fromBytesAutoDetect(data); // eslint-disable-next-line no-empty
127
- } catch (exception) {}
128
-
129
- const severity = status == 401 || status == 403 ? shaka$1.util.Error.Severity.CRITICAL : shaka$1.util.Error.Severity.RECOVERABLE;
130
- 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);
131
- };
132
-
133
- const goog$2 = {
134
- asserts: {
135
- assert: () => {}
136
- }
137
- };
138
- /**
139
- * @summary A networking plugin to handle http and https URIs via the Fetch API.
140
- * @export
141
- */
142
-
143
- class HttpFetchPlugin {
144
- /**
145
- * @param {string} uri
146
- * @param {shaka.extern.Request} request
147
- * @param {shaka.net.NetworkingEngine.RequestType} requestType
148
- * @param {shaka.extern.ProgressUpdated} progressUpdated Called when a
149
- * progress event happened.
150
- * @param {shaka.extern.HeadersReceived} headersReceived Called when the
151
- * headers for the download are received, but before the body is.
152
- * @return {!shaka.extern.IAbortableOperation.<shaka.extern.Response>}
153
- * @export
154
- */
155
- static parse(uri, request, requestType, progressUpdated, headersReceived) {
156
- const headers = new HttpFetchPlugin.Headers_();
157
- asMap(request.headers).forEach((value, key) => {
158
- headers.append(key, value);
159
- });
160
- const controller = new HttpFetchPlugin.AbortController_();
161
- /** @type {!RequestInit} */
162
-
163
- const init = {
164
- // Edge does not treat null as undefined for body; https://bit.ly/2luyE6x
165
- body: request.body || undefined,
166
- headers,
167
- method: request.method,
168
- signal: controller.signal,
169
- credentials: request.allowCrossSiteCredentials ? 'include' : undefined
170
- };
171
- /** @type {shaka.net.HttpFetchPlugin.AbortStatus} */
172
-
173
- const abortStatus = {
174
- canceled: false,
175
- timedOut: false
176
- };
177
- const pendingRequest = HttpFetchPlugin.request_(uri, requestType, init, abortStatus, progressUpdated, headersReceived, request.streamDataCallback);
178
- /** @type {!shaka.util.AbortableOperation} */
179
-
180
- const op = new shaka$1.util.AbortableOperation(pendingRequest, () => {
181
- abortStatus.canceled = true;
182
- controller.abort();
183
- return Promise.resolve();
184
- }); // The fetch API does not timeout natively, so do a timeout manually using
185
- // the AbortController.
186
-
187
- const timeoutMs = request.retryParameters.timeout;
188
-
189
- if (timeoutMs) {
190
- const timer = new shaka$1.util.Timer(() => {
191
- abortStatus.timedOut = true;
192
- controller.abort();
193
- });
194
- timer.tickAfter(timeoutMs / 1000); // To avoid calling |abort| on the network request after it finished, we
195
- // will stop the timer when the requests resolves/rejects.
196
-
197
- op.finally(() => {
198
- timer.stop();
199
- });
200
- }
201
-
202
- return op;
203
- }
204
- /**
205
- * @param {string} uri
206
- * @param {shaka.net.NetworkingEngine.RequestType} requestType
207
- * @param {!RequestInit} init
208
- * @param {shaka.net.HttpFetchPlugin.AbortStatus} abortStatus
209
- * @param {shaka.extern.ProgressUpdated} progressUpdated
210
- * @param {shaka.extern.HeadersReceived} headersReceived
211
- * @param {?function(BufferSource):!Promise} streamDataCallback
212
- * @return {!Promise<!shaka.extern.Response>}
213
- * @private
214
- */
215
-
216
-
217
- static async request_(uri, requestType, init, abortStatus, progressUpdated, headersReceived, streamDataCallback) {
218
- const fetch = HttpFetchPlugin.fetch_;
219
- const ReadableStream = HttpFetchPlugin.ReadableStream_;
220
- let response;
221
- let arrayBuffer;
222
- let loaded = 0;
223
- let lastLoaded = 0; // Last time stamp when we got a progress event.
224
-
225
- let lastTime = Date.now();
226
-
227
- try {
228
- // The promise returned by fetch resolves as soon as the HTTP response
229
- // headers are available. The download itself isn't done until the promise
230
- // for retrieving the data (arrayBuffer, blob, etc) has resolved.
231
- response = await fetch(uri, init); // At this point in the process, we have the headers of the response, but
232
- // not the body yet.
233
-
234
- headersReceived(HttpFetchPlugin.headersToGenericObject_(response.headers)); // Getting the reader in this way allows us to observe the process of
235
- // downloading the body, instead of just waiting for an opaque promise to
236
- // resolve.
237
- // We first clone the response because calling getReader locks the body
238
- // stream; if we didn't clone it here, we would be unable to get the
239
- // response's arrayBuffer later.
240
-
241
- const reader = response.clone().body.getReader();
242
- const contentLengthRaw = response.headers.get('Content-Length');
243
- const contentLength = contentLengthRaw ? parseInt(contentLengthRaw, 10) : 0;
244
-
245
- const start = controller => {
246
- const push = async () => {
247
- let readObj;
248
-
249
- try {
250
- readObj = await reader.read();
251
- } catch (e) {
252
- // If we abort the request, we'll get an error here. Just ignore it
253
- // since real errors will be reported when we read the buffer below.
254
- shakaLog$1.v1('error reading from stream', e.message);
255
- return;
256
- }
257
-
258
- if (!readObj.done) {
259
- loaded += readObj.value.byteLength; // streamDataCallback adds stream data to buffer for low latency mode
260
- // 4xx response means a segment is not ready and can retry soon
261
- // only successful response data should be added, or playback freezes
262
-
263
- if (response.status === 200 && streamDataCallback) {
264
- await streamDataCallback(readObj.value);
265
- }
266
- }
267
-
268
- const currentTime = Date.now(); // If the time between last time and this time we got progress event
269
- // is long enough, or if a whole segment is downloaded, call
270
- // progressUpdated().
271
-
272
- if (currentTime - lastTime > 100 || readObj.done) {
273
- progressUpdated(currentTime - lastTime, loaded - lastLoaded, contentLength - loaded);
274
- lastLoaded = loaded;
275
- lastTime = currentTime;
276
- }
277
-
278
- if (readObj.done) {
279
- goog$2.asserts.assert(!readObj.value, 'readObj should be unset when "done" is true.');
280
- controller.close();
281
- } else {
282
- controller.enqueue(readObj.value);
283
- push();
284
- }
285
- };
286
-
287
- push();
288
- }; // Create a ReadableStream to use the reader. We don't need to use the
289
- // actual stream for anything, though, as we are using the response's
290
- // arrayBuffer method to get the body, so we don't store the
291
- // ReadableStream.
292
-
293
-
294
- new ReadableStream({
295
- start
296
- }); // eslint-disable-line no-new
297
-
298
- arrayBuffer = await response.arrayBuffer();
299
- } catch (error) {
300
- if (abortStatus.canceled) {
301
- 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);
302
- } else if (abortStatus.timedOut) {
303
- 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);
304
- } else {
305
- 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);
306
- }
307
- }
308
-
309
- const headers = HttpFetchPlugin.headersToGenericObject_(response.headers);
310
- return makeResponse(headers, arrayBuffer, response.status, uri, response.url, requestType);
311
- }
312
- /**
313
- * @param {!Headers} headers
314
- * @return {!Object.<string, string>}
315
- * @private
316
- */
317
-
318
-
319
- static headersToGenericObject_(headers) {
320
- const headersObj = {};
321
- headers.forEach((value, key) => {
322
- // Since Edge incorrectly return the header with a leading new line
323
- // character ('\n'), we trim the header here.
324
- headersObj[key.trim()] = value;
325
- });
326
- return headersObj;
327
- }
328
-
329
- }
330
-
331
- HttpFetchPlugin.register = shakaNamespace => {
332
- shaka$1 = shakaNamespace;
333
- /**
334
- * Overridden in unit tests, but compiled out in production.
335
- *
336
- * @const {function(string, !RequestInit)}
337
- * @private
338
- */
339
-
340
- HttpFetchPlugin.fetch_ = window.fetch;
341
- /**
342
- * Overridden in unit tests, but compiled out in production.
343
- *
344
- * @const {function(new: AbortController)}
345
- * @private
346
- */
347
-
348
- HttpFetchPlugin.AbortController_ = window.AbortController;
349
- /**
350
- * Overridden in unit tests, but compiled out in production.
351
- *
352
- * @const {function(new: ReadableStream, !Object)}
353
- * @private
354
- */
355
-
356
- HttpFetchPlugin.ReadableStream_ = window.ReadableStream;
357
- /**
358
- * Overridden in unit tests, but compiled out in production.
359
- *
360
- * @const {function(new: Headers)}
361
- * @private
362
- */
363
-
364
- HttpFetchPlugin.Headers_ = window.Headers;
365
- shaka$1.net.NetworkingEngine.registerScheme('http', HttpFetchPlugin.parse);
366
- shaka$1.net.NetworkingEngine.registerScheme('https', HttpFetchPlugin.parse);
367
- shaka$1.net.NetworkingEngine.registerScheme('blob', HttpFetchPlugin.parse);
368
- };
369
-
370
- const defaultInitDataTransform = (initData, initDataType, drmInfo) => {
371
- if (initDataType === 'skd') {
372
- const {
373
- defaultGetContentId,
374
- initDataTransform
375
- } = shaka.util.FairPlayUtils;
376
- const cert = drmInfo.serverCertificate;
377
- const contentId = defaultGetContentId(initData);
378
- return initDataTransform(initData, contentId, cert);
379
- }
380
-
381
- return initData;
382
- };
383
-
384
- const wrapFairplayLicenseRequest = request => {
385
- const base64Payload = encodeURIComponent(btoa(String.fromCharCode(...new Uint8Array(request.body))));
386
- const contentId = encodeURIComponent(new TextDecoder('utf-8').decode(request.initData).slice(6));
387
- request.headers['Content-Type'] = 'application/x-www-form-urlencoded';
388
- request.body = `spc=${base64Payload}&asset_id=${contentId}`;
389
- };
390
-
391
- const requestHandler = (type, request, player, extensionOptions) => {
392
- const {
393
- LICENSE,
394
- SERVER_CERTIFICATE
395
- } = shaka.net.NetworkingEngine.RequestType;
396
-
397
- if (type === SERVER_CERTIFICATE) {
398
- var _extensionOptions$drm;
399
-
400
- Object.assign(request.headers, (_extensionOptions$drm = extensionOptions.drm[player.drmInfo().keySystem]) === null || _extensionOptions$drm === void 0 ? void 0 : _extensionOptions$drm.headers);
401
- }
402
-
403
- if (type === LICENSE) {
404
- wrapFairplayLicenseRequest(request);
405
- }
406
-
407
- return request;
408
- };
409
-
410
- const stripResponseCkc = (type, response) => {
411
- if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) {
412
- return response;
413
- }
414
-
415
- const keyMessage = new TextDecoder('utf-8').decode(response.data).trim();
416
-
417
- if (keyMessage.slice(0, 5) === '<ckc>' && keyMessage.slice(-6) === '</ckc>') {
418
- response.data = Uint8Array.from(atob(keyMessage.slice(5, -6)), c => c.charCodeAt(0));
419
- }
420
-
421
- return response;
422
- };
423
-
424
- const setupKKFariplay = (player, extensionOptions) => {
425
- if (!window.WebKitMediaKeys) {
426
- return;
427
- }
428
-
429
- shaka.polyfill.PatchedMediaKeysApple.install();
430
- player.configure({
431
- drm: {
432
- initDataTransform: defaultInitDataTransform
433
- }
434
- });
435
- extensionOptions.requestHandlers.push(requestHandler);
436
- extensionOptions.responseHandlers.push(stripResponseCkc);
437
- };
438
-
439
- /**
440
- * Parses an XML duration string.
441
- * Negative values are not supported. Years and months are treated as exactly
442
- * 365 and 30 days respectively.
443
- * @param {string} durationString The duration string, e.g., "PT1H3M43.2S",
444
- * which means 1 hour, 3 minutes, and 43.2 seconds.
445
- * @return {?number} The parsed duration in seconds on success; otherwise,
446
- * return null.
447
- * @see {@link http://www.datypic.com/sc/xsd/t-xsd_duration.html}
448
- */
449
- const parseDuration = durationString => {
450
- if (!durationString) {
451
- return null;
452
- }
453
-
454
- const re = '^P(?:([0-9]*)Y)?(?:([0-9]*)M)?(?:([0-9]*)D)?' + '(?:T(?:([0-9]*)H)?(?:([0-9]*)M)?(?:([0-9.]*)S)?)?$';
455
- const matches = new RegExp(re).exec(durationString);
456
-
457
- if (!matches) {
458
- console.warning('Invalid duration string:', durationString);
459
- return null;
460
- } // Note: Number(null) == 0 but Number(undefined) == NaN.
461
-
462
-
463
- const years = Number(matches[1] || null);
464
- const months = Number(matches[2] || null);
465
- const days = Number(matches[3] || null);
466
- const hours = Number(matches[4] || null);
467
- const minutes = Number(matches[5] || null);
468
- const seconds = Number(matches[6] || null); // Assume a year always has 365 days and a month always has 30 days.
469
-
470
- const d = 60 * 60 * 24 * 365 * years + 60 * 60 * 24 * 30 * months + 60 * 60 * 24 * days + 60 * 60 * hours + 60 * minutes + seconds;
471
- return Number.isFinite(d) ? d : null;
472
- };
473
-
474
- const normalize = (doc, template, period) => {
475
- const segmentDuration = parseFloat(template.getAttribute('duration'), 10);
476
-
477
- if (!segmentDuration) {
478
- return;
479
- }
480
-
481
- const timescale = parseFloat(template.getAttribute('timescale'), 10);
482
- const periodDuration = parseDuration(period.getAttribute('duration'));
483
- const item = doc.createElement('S');
484
- item.setAttribute('d', segmentDuration);
485
- item.setAttribute('r', Math.ceil(periodDuration * timescale / segmentDuration) - 1);
486
- const timeline = doc.createElement('SegmentTimeline');
487
- timeline.appendChild(item);
488
- template.appendChild(timeline);
489
- template.removeAttribute('duration');
490
- };
491
-
492
- const fixDashManifest = (data, {
493
- minTimeShiftBufferDepth
494
- } = {}) => {
495
- const doc = new DOMParser().parseFromString(data, 'text/xml'); // only dynamic manifest needs timeShiftBufferDepth
496
-
497
- const root = doc.children[0];
498
-
499
- if (root.getAttribute('type') === 'dynamic') {
500
- if (root.getAttribute('timeShiftBufferDepth') < minTimeShiftBufferDepth) {
501
- root.setAttribute('timeShiftBufferDepth', minTimeShiftBufferDepth);
502
- }
503
- } else if (root.hasAttribute('timeShiftBufferDepth')) {
504
- root.removeAttribute('timeShiftBufferDepth');
505
- } // workaround multi-period segment template bug, normalize to segment timelines
506
-
507
-
508
- if (doc.querySelectorAll('Period').length > 1) {
509
- Array.from(doc.querySelectorAll('Period')).forEach(period => {
510
- Array.from(period.querySelectorAll('SegmentTemplate')).forEach(template => normalize(doc, template, period));
511
- });
512
- }
513
-
514
- window.manifestDoc = doc;
515
- return new XMLSerializer().serializeToString(doc);
516
- };
517
-
518
- /* eslint-disable no-cond-assign */
519
-
520
- /* eslint-disable prefer-destructuring */
521
-
522
- /*! @license
523
- * Shaka Player
524
- * Copyright 2016 Google LLC
525
- * SPDX-License-Identifier: Apache-2.0
526
- */
527
- const goog$1 = {
528
- asserts: {
529
- assert: (condition, message) => {
530
- if (!condition) {
531
- console.warn('GOOG Assert!', message);
532
- }
533
- }
534
- }
535
- };
536
-
537
- const addDefaultTextColor = styles => {
538
- const textColor = shaka.text.Cue.defaultTextColor;
539
-
540
- for (const [key, value] of Object.entries(textColor)) {
541
- const cue = new shaka.text.Cue(0, 0, '');
542
- cue.color = value;
543
- styles.set(`.${key}`, cue);
544
- }
545
-
546
- const bgColor = shaka.text.Cue.defaultTextBackgroundColor;
547
-
548
- for (const [key, value] of Object.entries(bgColor)) {
549
- const cue = new shaka.text.Cue(0, 0, '');
550
- cue.backgroundColor = value;
551
- styles.set(`.${key}`, cue);
552
- }
553
- };
554
-
555
- const parseTime = text => {
556
- var _Array$from;
557
-
558
- const timeFormat = /(?:(\d{1,}):)?(\d{2}):(\d{2})((\.(\d{1,3})))?/g;
559
- const results = (_Array$from = Array.from(text.matchAll(timeFormat))) === null || _Array$from === void 0 ? void 0 : _Array$from[0];
560
-
561
- if (results == null) {
562
- return null;
563
- } // This capture is optional, but will still be in the array as undefined,
564
- // in which case it is 0.
565
-
566
-
567
- const hours = Number(results[1]) || 0;
568
- const minutes = Number(results[2]);
569
- const seconds = Number(results[3]);
570
- const milliseconds = Number(results[6]) || 0;
571
-
572
- if (minutes > 59 || seconds > 59) {
573
- return null;
574
- }
575
-
576
- return milliseconds / 1000 + seconds + minutes * 60 + hours * 3600;
577
- };
578
-
579
- const parseRegion = block => {
580
- if (block[0].trim() !== 'REGION') {
581
- return [];
582
- }
583
-
584
- const region = new shaka.text.CueRegion();
585
- block.slice(1).forEach(word => {
586
- let results = null;
587
-
588
- if (results = /^id:(.*)$/.exec(word)) {
589
- region.id = results[1];
590
- } else if (results = /^width:(\d{1,2}|100)%$/.exec(word)) {
591
- region.width = Number(results[1]);
592
- } else if (results = /^lines:(\d+)$/.exec(word)) {
593
- region.height = Number(results[1]);
594
- region.heightUnits = shaka.text.CueRegion.units.LINES;
595
- } else if (results = /^regionanchor:(\d{1,2}|100)%,(\d{1,2}|100)%$/.exec(word)) {
596
- region.regionAnchorX = Number(results[1]);
597
- region.regionAnchorY = Number(results[2]);
598
- } else if (results = /^viewportanchor:(\d{1,2}|100)%,(\d{1,2}|100)%$/.exec(word)) {
599
- region.viewportAnchorX = Number(results[1]);
600
- region.viewportAnchorY = Number(results[2]);
601
- } else if (results = /^scroll:up$/.exec(word)) {
602
- region.scroll = shaka.text.CueRegion.scrollMode.UP;
603
- } else {
604
- shaka.log.warning('VTT parser encountered an invalid VTTRegion setting: ', word, ' The setting will be ignored.');
605
- }
606
- });
607
- return [region];
608
- };
609
- /**
610
- * @implements {shaka.extern.TextParser}
611
- * @export
612
- */
613
-
614
-
615
- class VttTextParser {
616
- /** Constructs a VTT parser. */
617
- constructor() {
618
- /** @private {boolean} */
619
- this.sequenceMode_ = false;
620
- /** @private {string} */
621
-
622
- this.manifestType_ = shaka.media.ManifestParser.UNKNOWN;
623
- }
624
- /**
625
- * @override
626
- * @export
627
- */
628
-
629
-
630
- parseInit() {
631
- goog$1.asserts.assert(false, 'VTT does not have init segments');
632
- }
633
- /**
634
- * @override
635
- * @export
636
- */
637
-
638
-
639
- setSequenceMode(sequenceMode) {
640
- this.sequenceMode_ = sequenceMode;
641
- }
642
- /**
643
- * @override
644
- * @export
645
- */
646
-
647
-
648
- setManifestType(manifestType) {
649
- this.manifestType_ = manifestType;
650
- }
651
- /**
652
- * @override
653
- * @export
654
- */
655
-
656
-
657
- parseMedia(data, time) {
658
- // Get the input as a string. Normalize newlines to \n.
659
- let str = shaka.util.StringUtils.fromUTF8(data);
660
- str = str.replace(/\r\n|\r(?=[^\n]|$)/gm, '\n');
661
- const blocks = str.split(/\n{2,}/m);
662
-
663
- if (!/^WEBVTT($|[ \t\n])/m.test(blocks[0])) {
664
- throw new shaka.util.Error(shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.TEXT, shaka.util.Error.Code.INVALID_TEXT_HEADER);
665
- } // Depending on "segmentRelativeVttTiming" configuration,
666
- // "vttOffset" will correspond to either "periodStart" (default)
667
- // or "segmentStart", for segmented VTT where timings are relative
668
- // to the beginning of each segment.
669
- // NOTE: "periodStart" is the timestamp offset applied via TextEngine.
670
- // It is no longer closely tied to periods, but the name stuck around.
671
- // NOTE: This offset and the flag choosing its meaning have no effect on
672
- // HLS content, which should use X-TIMESTAMP-MAP and periodStart instead.
673
-
674
-
675
- let offset = time.vttOffset;
676
-
677
- if (this.manifestType_ == shaka.media.ManifestParser.HLS) {
678
- // Only use 'X-TIMESTAMP-MAP' with HLS.
679
- if (blocks[0].includes('X-TIMESTAMP-MAP')) {
680
- offset = this.computeHlsOffset_(blocks[0], time);
681
- } else if (time.periodStart && time.vttOffset == time.periodStart) {
682
- // In the case where X-TIMESTAMP-MAP is not used and it is HLS, we
683
- // should not use offset unless segment-relative times are used.
684
- offset = 0;
685
- }
686
- }
687
-
688
- const regions = [];
689
- /** @type {!Map<string, !shaka.text.Cue>} */
690
-
691
- const styles = new Map();
692
- addDefaultTextColor(styles); // Parse cues.
693
-
694
- const ret = [];
695
-
696
- for (const block of blocks.slice(1)) {
697
- const lines = block.split('\n');
698
- VttTextParser.parseStyle_(lines, styles);
699
- regions.push(...parseRegion(lines));
700
- const cue = VttTextParser.parseCue_(lines, offset, regions, styles);
701
-
702
- if (cue) {
703
- ret.push(cue);
704
- }
705
- }
706
-
707
- return ret;
708
- }
709
- /**
710
- * @param {string} headerBlock Contains X-TIMESTAMP-MAP.
711
- * @param {shaka.extern.TextParser.TimeContext} time
712
- * @return {number}
713
- * @private
714
- */
715
-
716
-
717
- computeHlsOffset_(headerBlock, time) {
718
- // https://bit.ly/2K92l7y
719
- // The 'X-TIMESTAMP-MAP' header is used in HLS to align text with
720
- // the rest of the media.
721
- // The header format is 'X-TIMESTAMP-MAP=MPEGTS:n,LOCAL:m'
722
- // (the attributes can go in any order)
723
- // where n is MPEG-2 time and m is cue time it maps to.
724
- // For example 'X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:900000'
725
- // means an offset of 10 seconds
726
- // 900000/MPEG_TIMESCALE - cue time.
727
- const cueTimeMatch = headerBlock.match(/LOCAL:((?:(\d{1,}):)?(\d{2}):(\d{2})\.(\d{3}))/m);
728
- const mpegTimeMatch = headerBlock.match(/MPEGTS:(\d+)/m);
729
-
730
- if (!cueTimeMatch || !mpegTimeMatch) {
731
- throw new shaka.util.Error(shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.TEXT, shaka.util.Error.Code.INVALID_TEXT_HEADER);
732
- }
733
-
734
- const cueTime = parseTime(cueTimeMatch[1]);
735
-
736
- if (cueTime == null) {
737
- throw new shaka.util.Error(shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.TEXT, shaka.util.Error.Code.INVALID_TEXT_HEADER);
738
- }
739
-
740
- let mpegTime = Number(mpegTimeMatch[1]);
741
- const mpegTimescale = VttTextParser.MPEG_TIMESCALE_;
742
- const rolloverSeconds = VttTextParser.TS_ROLLOVER_ / mpegTimescale;
743
- let segmentStart = time.segmentStart - time.periodStart;
744
-
745
- while (segmentStart >= rolloverSeconds) {
746
- segmentStart -= rolloverSeconds;
747
- mpegTime += VttTextParser.TS_ROLLOVER_;
748
- }
749
-
750
- return time.periodStart + mpegTime / mpegTimescale - cueTime;
751
- }
752
- /**
753
- * Parses a style block into a Cue object.
754
- *
755
- * @param {!Array<string>} text
756
- * @param {!Map<string, !shaka.text.Cue>} styles
757
- * @private
758
- */
759
-
760
-
761
- static parseStyle_(text, styles) {
762
- // Skip empty blocks.
763
- if (text.length == 1 && !text[0]) {
764
- return;
765
- } // Skip comment blocks.
766
-
767
-
768
- if (/^NOTE($|[ \t])/.test(text[0])) {
769
- return;
770
- } // Only style block are allowed.
771
-
772
-
773
- if (text[0] != 'STYLE') {
774
- return;
775
- }
776
- /** @type {!Array<!Array<string>>} */
777
-
778
-
779
- const styleBlocks = [];
780
- let lastBlockIndex = -1;
781
-
782
- for (let i = 1; i < text.length; i++) {
783
- if (text[i].includes('::cue')) {
784
- styleBlocks.push([]);
785
- lastBlockIndex = styleBlocks.length - 1;
786
- }
787
-
788
- if (lastBlockIndex == -1) {
789
- continue;
790
- }
791
-
792
- styleBlocks[lastBlockIndex].push(text[i]);
793
-
794
- if (text[i].includes('}')) {
795
- lastBlockIndex = -1;
796
- }
797
- }
798
-
799
- for (const styleBlock of styleBlocks) {
800
- let styleSelector = 'global'; // Look for what is within parentheses. For example:
801
- // <code>:: cue (b) {</code>, what we are looking for is <code>b</code>
802
-
803
- const selector = styleBlock[0].match(/\((.*)\)/);
804
-
805
- if (selector) {
806
- styleSelector = selector.pop();
807
- } // We start at 1 to avoid '::cue' and end earlier to avoid '}'
808
-
809
-
810
- let propertyLines = styleBlock.slice(1, -1);
811
-
812
- if (styleBlock[0].includes('}')) {
813
- const payload = /\{(.*?)\}/.exec(styleBlock[0]);
814
-
815
- if (payload) {
816
- propertyLines = payload[1].split(';');
817
- }
818
- } // Continue styles over multiple selectors if necessary.
819
- // For example,
820
- // ::cue(b) { background: white; } ::cue(b) { color: blue; }
821
- // should set both the background and foreground of bold tags.
822
-
823
-
824
- let cue = styles.get(styleSelector);
825
-
826
- if (!cue) {
827
- cue = new shaka.text.Cue(0, 0, '');
828
- }
829
-
830
- let validStyle = false;
831
-
832
- for (let i = 0; i < propertyLines.length; i++) {
833
- // We look for CSS properties. As a general rule they are separated by
834
- // <code>:</code>. Eg: <code>color: red;</code>
835
- const lineParts = /^\s*([^:]+):\s*(.*)/.exec(propertyLines[i]);
836
-
837
- if (lineParts) {
838
- const name = lineParts[1].trim();
839
- const value = lineParts[2].trim().replace(';', '');
840
-
841
- switch (name) {
842
- case 'background-color':
843
- case 'background':
844
- validStyle = true;
845
- cue.backgroundColor = value;
846
- break;
847
-
848
- case 'color':
849
- validStyle = true;
850
- cue.color = value;
851
- break;
852
-
853
- case 'font-family':
854
- validStyle = true;
855
- cue.fontFamily = value;
856
- break;
857
-
858
- case 'font-size':
859
- validStyle = true;
860
- cue.fontSize = value;
861
- break;
862
-
863
- case 'font-weight':
864
- if (parseInt(value, 10) >= 700 || value == 'bold') {
865
- validStyle = true;
866
- cue.fontWeight = shaka.text.Cue.fontWeight.BOLD;
867
- }
868
-
869
- break;
870
-
871
- case 'font-style':
872
- switch (value) {
873
- case 'normal':
874
- validStyle = true;
875
- cue.fontStyle = shaka.text.Cue.fontStyle.NORMAL;
876
- break;
877
-
878
- case 'italic':
879
- validStyle = true;
880
- cue.fontStyle = shaka.text.Cue.fontStyle.ITALIC;
881
- break;
882
-
883
- case 'oblique':
884
- validStyle = true;
885
- cue.fontStyle = shaka.text.Cue.fontStyle.OBLIQUE;
886
- break;
887
- }
888
-
889
- break;
890
-
891
- case 'opacity':
892
- validStyle = true;
893
- cue.opacity = parseFloat(value);
894
- break;
895
-
896
- case 'text-combine-upright':
897
- validStyle = true;
898
- cue.textCombineUpright = value;
899
- break;
900
-
901
- case 'text-shadow':
902
- validStyle = true;
903
- cue.textShadow = value;
904
- break;
905
-
906
- case 'white-space':
907
- validStyle = true;
908
- cue.wrapLine = value != 'noWrap';
909
- break;
910
-
911
- default:
912
- shaka.log.warning('VTT parser encountered an unsupported style: ', lineParts);
913
- break;
914
- }
915
- }
916
- }
917
-
918
- if (validStyle) {
919
- styles.set(styleSelector, cue);
920
- }
921
- }
922
- }
923
- /**
924
- * Parses a text block into a Cue object.
925
- *
926
- * @param {!Array<string>} text
927
- * @param {number} timeOffset
928
- * @param {!Array<!shaka.text.CueRegion>} regions
929
- * @param {!Map<string, !shaka.text.Cue>} styles
930
- * @return {shaka.text.Cue}
931
- * @private
932
- */
933
-
934
-
935
- static parseCue_(text, timeOffset, regions, styles) {
936
- // Skip empty blocks.
937
- if (text.length == 1 && !text[0]) {
938
- return null;
939
- } // Skip comment blocks.
940
-
941
-
942
- if (/^NOTE($|[ \t])/.test(text[0])) {
943
- return null;
944
- } // Skip style and region blocks.
945
-
946
-
947
- if (text[0] == 'STYLE' || text[0] == 'REGION') {
948
- return null;
949
- }
950
-
951
- const skipIndex = text.findIndex(line => /^#/.test(line.trim()));
952
-
953
- if (skipIndex > 0) {
954
- text = text.slice(skipIndex);
955
- }
956
-
957
- if (text.length < 2) {
958
- return;
959
- }
960
-
961
- let id = null;
962
-
963
- if (!text[0].includes('-->')) {
964
- id = text[0];
965
- text.splice(0, 1);
966
- } // Parse the times.
967
-
968
-
969
- let [start, end] = text[0].split('-->').map(part => parseTime(part.trim()));
970
-
971
- if (start == null || end == null) {
972
- shaka.log.alwaysWarn('Failed to parse VTT time code. Cue skipped:', id, text);
973
- return null;
974
- }
975
-
976
- start += timeOffset;
977
- end += timeOffset; // Get the payload.
978
-
979
- const payload = text.slice(1).join('\n').trim();
980
- let cue = null;
981
-
982
- if (styles.has('global')) {
983
- cue = styles.get('global').clone();
984
- cue.startTime = start;
985
- cue.endTime = end;
986
- cue.payload = payload;
987
- } else {
988
- cue = new shaka.text.Cue(start, end, payload);
989
- } // Parse optional settings.
990
-
991
-
992
- text[0].split(/\s+/g).slice(3).forEach(word => {
993
- if (!word.trim()) {
994
- return;
995
- }
996
-
997
- if (!VttTextParser.parseCueSetting(cue, word, regions)) {
998
- shaka.log.warning('VTT parser encountered an invalid VTT setting: ', word, ' The setting will be ignored.');
999
- }
1000
- });
1001
- shaka.text.Cue.parseCuePayload(cue, styles);
1002
-
1003
- if (id != null) {
1004
- cue.id = id;
1005
- }
1006
-
1007
- return cue;
1008
- }
1009
- /**
1010
- * Parses a WebVTT setting from the given word.
1011
- *
1012
- * @param {!shaka.text.Cue} cue
1013
- * @param {string} word
1014
- * @param {!Array<!shaka.text.CueRegion>} regions
1015
- * @return {boolean} True on success.
1016
- */
1017
-
1018
-
1019
- static parseCueSetting(cue, word, regions) {
1020
- let results = null;
1021
-
1022
- if (results = /^align:(start|middle|center|end|left|right)$/.exec(word)) {
1023
- VttTextParser.setTextAlign_(cue, results[1]);
1024
- } else if (results = /^vertical:(lr|rl)$/.exec(word)) {
1025
- VttTextParser.setVerticalWritingMode_(cue, results[1]);
1026
- } else if (results = /^size:([\d.]+)%$/.exec(word)) {
1027
- cue.size = Number(results[1]);
1028
- } else if (results = /^position:([\d.]+)%(?:,(line-left|line-right|middle|center|start|end|auto))?$/.exec(word)) {
1029
- cue.position = Number(results[1]);
1030
-
1031
- if (results[2]) {
1032
- VttTextParser.setPositionAlign_(cue, results[2]);
1033
- }
1034
- } else if (results = /^region:(.*)$/.exec(word)) {
1035
- const region = VttTextParser.getRegionById_(regions, results[1]);
1036
-
1037
- if (region) {
1038
- cue.region = region;
1039
- }
1040
- } else {
1041
- return VttTextParser.parsedLineValueAndInterpretation_(cue, word);
1042
- }
1043
-
1044
- return true;
1045
- }
1046
- /**
1047
- *
1048
- * @param {!Array<!shaka.text.CueRegion>} regions
1049
- * @param {string} id
1050
- * @return {?shaka.text.CueRegion}
1051
- * @private
1052
- */
1053
-
1054
-
1055
- static getRegionById_(regions, id) {
1056
- const regionsWithId = regions.filter(region => region.id == id);
1057
-
1058
- if (!regionsWithId.length) {
1059
- shaka.log.warning('VTT parser could not find a region with id: ', id, ' The region will be ignored.');
1060
- return null;
1061
- }
1062
-
1063
- goog$1.asserts.assert(regionsWithId.length == 1, 'VTTRegion ids should be unique!');
1064
- return regionsWithId[0];
1065
- }
1066
- /**
1067
- * @param {!shaka.text.Cue} cue
1068
- * @param {string} align
1069
- * @private
1070
- */
1071
-
1072
-
1073
- static setTextAlign_(cue, align) {
1074
- const Cue = shaka.text.Cue;
1075
-
1076
- if (align == 'middle') {
1077
- cue.textAlign = Cue.textAlign.CENTER;
1078
- } else {
1079
- goog$1.asserts.assert(align.toUpperCase() in Cue.textAlign, `${align.toUpperCase()} Should be in Cue.textAlign values!`);
1080
- cue.textAlign = Cue.textAlign[align.toUpperCase()];
1081
- }
1082
- }
1083
- /**
1084
- * @param {!shaka.text.Cue} cue
1085
- * @param {string} align
1086
- * @private
1087
- */
1088
-
1089
-
1090
- static setPositionAlign_(cue, align) {
1091
- const Cue = shaka.text.Cue;
1092
-
1093
- if (align == 'line-left' || align == 'start') {
1094
- cue.positionAlign = Cue.positionAlign.LEFT;
1095
- } else if (align == 'line-right' || align == 'end') {
1096
- cue.positionAlign = Cue.positionAlign.RIGHT;
1097
- } else if (align == 'center' || align == 'middle') {
1098
- cue.positionAlign = Cue.positionAlign.CENTER;
1099
- } else {
1100
- cue.positionAlign = Cue.positionAlign.AUTO;
1101
- }
1102
- }
1103
- /**
1104
- * @param {!shaka.text.Cue} cue
1105
- * @param {string} value
1106
- * @private
1107
- */
1108
-
1109
-
1110
- static setVerticalWritingMode_(cue, value) {
1111
- const Cue = shaka.text.Cue;
1112
-
1113
- if (value == 'lr') {
1114
- cue.writingMode = Cue.writingMode.VERTICAL_LEFT_TO_RIGHT;
1115
- } else {
1116
- cue.writingMode = Cue.writingMode.VERTICAL_RIGHT_TO_LEFT;
1117
- }
1118
- }
1119
- /**
1120
- * @param {!shaka.text.Cue} cue
1121
- * @param {string} word
1122
- * @return {boolean}
1123
- * @private
1124
- */
1125
-
1126
-
1127
- static parsedLineValueAndInterpretation_(cue, word) {
1128
- const Cue = shaka.text.Cue;
1129
- let results = null;
1130
-
1131
- if (results = /^line:([\d.]+)%(?:,(start|end|center))?$/.exec(word)) {
1132
- cue.lineInterpretation = Cue.lineInterpretation.PERCENTAGE;
1133
- cue.line = Number(results[1]);
1134
-
1135
- if (results[2]) {
1136
- goog$1.asserts.assert(results[2].toUpperCase() in Cue.lineAlign, `${results[2].toUpperCase()} Should be in Cue.lineAlign values!`);
1137
- cue.lineAlign = Cue.lineAlign[results[2].toUpperCase()];
1138
- }
1139
- } else if (results = /^line:(-?\d+)(?:,(start|end|center))?$/.exec(word)) {
1140
- cue.lineInterpretation = Cue.lineInterpretation.LINE_NUMBER;
1141
- cue.line = Number(results[1]);
1142
-
1143
- if (results[2]) {
1144
- goog$1.asserts.assert(results[2].toUpperCase() in Cue.lineAlign, `${results[2].toUpperCase()} Should be in Cue.lineAlign values!`);
1145
- cue.lineAlign = Cue.lineAlign[results[2].toUpperCase()];
1146
- }
1147
- } else {
1148
- return false;
1149
- }
1150
-
1151
- return true;
1152
- }
1153
-
1154
- }
1155
- /**
1156
- * @const {number}
1157
- * @private
1158
- */
1159
-
1160
-
1161
- VttTextParser.MPEG_TIMESCALE_ = 90000;
1162
- /**
1163
- * At this value, timestamps roll over in TS content.
1164
- * @const {number}
1165
- * @private
1166
- */
1167
-
1168
- VttTextParser.TS_ROLLOVER_ = 0x200000000;
1169
-
1170
- VttTextParser.register = shakaNameSpace => {
1171
- shakaNameSpace.text.TextEngine.registerParser('text/vtt', () => new VttTextParser());
1172
- shakaNameSpace.text.TextEngine.registerParser('text/vtt; codecs="vtt"', () => new VttTextParser());
1173
- shakaNameSpace.text.TextEngine.registerParser('text/vtt; codecs="wvtt"', () => new VttTextParser());
1174
- };
1175
-
1176
- /*! @license
1177
- * Shaka Player
1178
- * Copyright 2016 Google LLC
1179
- * SPDX-License-Identifier: Apache-2.0
1180
- */
1181
- const shakaLog = {
1182
- debug: (...messages) => console.warn(...messages),
1183
- error: (...messages) => console.warn(...messages),
1184
- info: (...messages) => console.warn(...messages),
1185
- warning: (...messages) => console.warn(...messages),
1186
- alwaysWarn: (...messages) => console.warn(...messages)
1187
- };
1188
- const goog = {
1189
- asserts: {
1190
- assert: (result, message) => {
1191
- result || console.warn(message);
1192
- }
1193
- }
1194
- };
1195
- /**
1196
- * Returns info about provided lengthValue
1197
- * @example 100px => { value: 100, unit: 'px' }
1198
- * @param {?string} lengthValue
1199
- *
1200
- * @return {?{ value: number, unit: string }}
1201
- * @private
1202
- */
1203
-
1204
- const getLengthValueInfo_ = lengthValue => {
1205
- const matches = /(\d*\.?\d+)([a-z]+|%+)/.exec(lengthValue);
1206
-
1207
- if (!matches) {
1208
- return null;
1209
- }
1210
-
1211
- return {
1212
- value: Number(matches[1]),
1213
- unit: matches[2]
1214
- };
1215
- };
1216
- /**
1217
- * Returns computed absolute length value in pixels based on cell
1218
- * and a video container size
1219
- * @param {number} value
1220
- * @param {!shaka.text.Cue} cue
1221
- * @param {HTMLElement} videoContainer
1222
- * @return {string}
1223
- *
1224
- * @private
1225
- */
1226
-
1227
-
1228
- const getAbsoluteLengthInPixels_ = (value, cue, videoContainer) => {
1229
- const containerHeight = videoContainer.clientHeight;
1230
- return `${containerHeight * value / cue.cellResolution.rows}px`;
1231
- };
1232
- /**
1233
- * Inherits a property from the parent Cue elements. If the value is falsy,
1234
- * it is assumed to be inherited from the parent. This returns null if the
1235
- * value isn't found.
1236
- *
1237
- * @param {!Array<!shaka.text.Cue>} parents
1238
- * @param {function(!shaka.text.Cue):?T} cb
1239
- * @return {?T}
1240
- * @template T
1241
- * @private
1242
- */
1243
-
1244
-
1245
- const inheritProperty_ = (parents, cb) => {
1246
- for (let i = parents.length - 1; i >= 0; i--) {
1247
- const val = cb(parents[i]);
1248
-
1249
- if (val || val === 0) {
1250
- return val;
1251
- }
1252
- }
1253
-
1254
- return null;
1255
- };
1256
- /**
1257
- * Converts length value to an absolute value in pixels.
1258
- * If lengthValue is already an absolute value it will not
1259
- * be modified. Relative lengthValue will be converted to an
1260
- * absolute value in pixels based on Computed Cell Size
1261
- *
1262
- * @param {string} lengthValue
1263
- * @param {!shaka.text.Cue} cue
1264
- * @param {HTMLElement} videoContainer
1265
- * @return {string}
1266
- * @private
1267
- */
1268
-
1269
-
1270
- const convertLengthValue_ = (lengthValue, cue, videoContainer) => {
1271
- const lengthValueInfo = getLengthValueInfo_(lengthValue);
1272
-
1273
- if (!lengthValueInfo) {
1274
- return lengthValue;
1275
- }
1276
-
1277
- const {
1278
- unit,
1279
- value
1280
- } = lengthValueInfo;
1281
-
1282
- switch (unit) {
1283
- case '%':
1284
- return getAbsoluteLengthInPixels_(value / 100, cue, videoContainer);
1285
-
1286
- case 'c':
1287
- return getAbsoluteLengthInPixels_(value, cue, videoContainer);
1288
-
1289
- default:
1290
- return lengthValue;
1291
- }
1292
- };
1293
-
1294
- const removeDuplicates = cues => {
1295
- const uniqueCues = [];
1296
-
1297
- for (const cue of cues) {
1298
- const isValid = !uniqueCues.some(existingCue => shaka.text.Cue.equal(cue, existingCue));
1299
-
1300
- if (isValid) {
1301
- uniqueCues.push(cue);
1302
- }
1303
- }
1304
-
1305
- return uniqueCues;
1306
- };
1307
- /**
1308
- * The text displayer plugin for the Shaka Player UI. Can also be used directly
1309
- * by providing an appropriate container element.
1310
- *
1311
- * @implements {shaka.extern.TextDisplayer}
1312
- * @final
1313
- * @export
1314
- */
1315
-
1316
-
1317
- class UITextDisplayer {
1318
- /**
1319
- * Constructor.
1320
- * @param {HTMLMediaElement} video
1321
- * @param {HTMLElement} videoContainer
1322
- */
1323
- constructor(video, videoContainer) {
1324
- window.shaka.log = shakaLog;
1325
- goog.asserts.assert(videoContainer, 'videoContainer should be valid.');
1326
-
1327
- if (!document.fullscreenEnabled) {
1328
- shaka.log.alwaysWarn('Using UITextDisplayer in a browser without ' + 'Fullscreen API support causes subtitles to not be rendered in ' + 'fullscreen');
1329
- }
1330
- /** @private {boolean} */
1331
-
1332
-
1333
- this.isTextVisible_ = false;
1334
- /** @private {!Array<!shaka.text.Cue>} */
1335
-
1336
- this.cues_ = [];
1337
- /** @private {HTMLMediaElement} */
1338
-
1339
- this.video_ = video;
1340
- /** @private {HTMLElement} */
1341
-
1342
- this.videoContainer_ = videoContainer;
1343
- /** @private {?number} */
1344
-
1345
- this.aspectRatio_ = null;
1346
- /** @private {?shaka.extern.TextDisplayerConfiguration} */
1347
-
1348
- this.config_ = null;
1349
- /** @type {HTMLElement} */
1350
-
1351
- this.textContainer_ = document.createElement('div');
1352
- this.textContainer_.classList.add('shaka-text-container'); // Set the subtitles text-centered by default.
1353
-
1354
- this.textContainer_.style.textAlign = 'center'; // Set the captions in the middle horizontally by default.
1355
-
1356
- this.textContainer_.style.display = 'flex';
1357
- this.textContainer_.style.flexDirection = 'column';
1358
- this.textContainer_.style.alignItems = 'center'; // Set the captions at the bottom by default.
1359
-
1360
- this.textContainer_.style.justifyContent = 'flex-end';
1361
- this.videoContainer_.appendChild(this.textContainer_);
1362
- /** @private {shaka.util.Timer} */
1363
-
1364
- this.captionsTimer_ = new shaka.util.Timer(() => {
1365
- if (!this.video_.paused) {
1366
- this.updateCaptions_();
1367
- }
1368
- });
1369
- this.configureCaptionsTimer_();
1370
- /**
1371
- * Maps cues to cue elements. Specifically points out the wrapper element of
1372
- * the cue (e.g. the HTML element to put nested cues inside).
1373
- * @private {Map<!shaka.text.Cue, !{
1374
- * cueElement: !HTMLElement,
1375
- * regionElement: HTMLElement,
1376
- * wrapper: !HTMLElement
1377
- * }>}
1378
- */
1379
-
1380
- this.currentCuesMap_ = new Map();
1381
- /** @private {shaka.util.EventManager} */
1382
-
1383
- this.eventManager_ = new shaka.util.EventManager();
1384
- this.eventManager_.listen(document, 'fullscreenchange', () => {
1385
- this.updateCaptions_(
1386
- /* forceUpdate= */
1387
- true);
1388
- });
1389
- this.eventManager_.listen(this.video_, 'seeking', () => {
1390
- this.updateCaptions_(
1391
- /* forceUpdate= */
1392
- true);
1393
- });
1394
- this.eventManager_.listen(this.video_, 'ratechange', () => {
1395
- this.configureCaptionsTimer_();
1396
- }); // From: https://html.spec.whatwg.org/multipage/media.html#dom-video-videowidth
1397
- // Whenever the natural width or natural height of the video changes
1398
- // (including, for example, because the selected video track was changed),
1399
- // if the element's readyState attribute is not HAVE_NOTHING, the user
1400
- // agent must queue a media element task given the media element to fire an
1401
- // event named resize at the media element.
1402
-
1403
- this.eventManager_.listen(this.video_, 'resize', () => {
1404
- const element =
1405
- /** @type {!HTMLVideoElement} */
1406
- this.video_;
1407
- const width = element.videoWidth;
1408
- const height = element.videoHeight;
1409
-
1410
- if (width && height) {
1411
- this.aspectRatio_ = width / height;
1412
- } else {
1413
- this.aspectRatio_ = null;
1414
- }
1415
- });
1416
- /** @private {ResizeObserver} */
1417
-
1418
- this.resizeObserver_ = null;
1419
-
1420
- if ('ResizeObserver' in window) {
1421
- this.resizeObserver_ = new ResizeObserver(() => {
1422
- this.updateCaptions_(
1423
- /* forceUpdate= */
1424
- true);
1425
- });
1426
- this.resizeObserver_.observe(this.textContainer_);
1427
- }
1428
- /** @private {Map<string, !HTMLElement>} */
1429
-
1430
-
1431
- this.regionElements_ = new Map();
1432
- }
1433
- /**
1434
- * @override
1435
- * @export
1436
- */
1437
-
1438
-
1439
- configure(config) {
1440
- this.config_ = config;
1441
- this.configureCaptionsTimer_();
1442
- }
1443
- /**
1444
- * @override
1445
- * @export
1446
- */
1447
-
1448
-
1449
- append(cues) {
1450
- // Clone the cues list for performance optimization. We can avoid the cues
1451
- // list growing during the comparisons for duplicate cues.
1452
- // See: https://github.com/shaka-project/shaka-player/issues/3018
1453
- const cuesList = [...this.cues_];
1454
-
1455
- for (const cue of removeDuplicates(cues)) {
1456
- // When a VTT cue spans a segment boundary, the cue will be duplicated
1457
- // into two segments.
1458
- // To avoid displaying duplicate cues, if the current cue list already
1459
- // contains the cue, skip it.
1460
- const containsCue = cuesList.some(cueInList => shaka.text.Cue.equal(cueInList, cue));
1461
-
1462
- if (!containsCue) {
1463
- this.cues_.push(cue);
1464
- }
1465
- }
1466
-
1467
- this.updateCaptions_();
1468
- }
1469
- /**
1470
- * @override
1471
- * @export
1472
- */
1473
-
1474
-
1475
- destroy() {
1476
- // Return resolved promise if destroy() has been called.
1477
- if (!this.textContainer_) {
1478
- return Promise.resolve();
1479
- } // Remove the text container element from the UI.
1480
-
1481
-
1482
- this.videoContainer_.removeChild(this.textContainer_);
1483
- this.textContainer_ = null;
1484
- this.isTextVisible_ = false;
1485
- this.cues_ = [];
1486
-
1487
- if (this.captionsTimer_) {
1488
- this.captionsTimer_.stop();
1489
- this.captionsTimer_ = null;
1490
- }
1491
-
1492
- this.currentCuesMap_.clear(); // Tear-down the event manager to ensure messages stop moving around.
1493
-
1494
- if (this.eventManager_) {
1495
- this.eventManager_.release();
1496
- this.eventManager_ = null;
1497
- }
1498
-
1499
- if (this.resizeObserver_) {
1500
- this.resizeObserver_.disconnect();
1501
- this.resizeObserver_ = null;
1502
- }
1503
-
1504
- return Promise.resolve();
1505
- }
1506
- /**
1507
- * @override
1508
- * @export
1509
- */
1510
-
1511
-
1512
- remove(start, end) {
1513
- // Return false if destroy() has been called.
1514
- if (!this.textContainer_) {
1515
- return false;
1516
- } // Remove the cues out of the time range.
1517
-
1518
-
1519
- const oldNumCues = this.cues_.length;
1520
- this.cues_ = this.cues_.filter(cue => cue.startTime < start || cue.endTime >= end); // If anything was actually removed in this process, force the captions to
1521
- // update. This makes sure that the currently-displayed cues will stop
1522
- // displaying if removed (say, due to the user changing languages).
1523
-
1524
- const forceUpdate = oldNumCues > this.cues_.length;
1525
- this.updateCaptions_(forceUpdate);
1526
- return true;
1527
- }
1528
- /**
1529
- * @override
1530
- * @export
1531
- */
1532
-
1533
-
1534
- isTextVisible() {
1535
- return this.isTextVisible_;
1536
- }
1537
- /**
1538
- * @override
1539
- * @export
1540
- */
1541
-
1542
-
1543
- setTextVisibility(on) {
1544
- this.isTextVisible_ = on;
1545
- this.updateCaptions_(
1546
- /* forceUpdate= */
1547
- true);
1548
- }
1549
- /**
1550
- * @override
1551
- * @export
1552
- */
1553
-
1554
-
1555
- setTextLanguage(language) {
1556
- if (language && language != 'und') {
1557
- this.textContainer_.setAttribute('lang', language);
1558
- } else {
1559
- this.textContainer_.setAttribute('lang', '');
1560
- }
1561
- }
1562
- /**
1563
- * @override
1564
- * @export
1565
- */
1566
-
1567
-
1568
- enableTextDisplayer() {}
1569
- /**
1570
- * @private
1571
- */
1572
-
1573
-
1574
- configureCaptionsTimer_() {
1575
- if (this.captionsTimer_) {
1576
- const captionsUpdatePeriod = this.config_ ? this.config_.captionsUpdatePeriod : 0.25;
1577
- const updateTime = captionsUpdatePeriod / Math.max(1, Math.abs(this.video_.playbackRate));
1578
- this.captionsTimer_.tickEvery(updateTime);
1579
- }
1580
- }
1581
- /**
1582
- * @private
1583
- */
1584
-
1585
-
1586
- isElementUnderTextContainer_(elemToCheck) {
1587
- while (elemToCheck != null) {
1588
- if (elemToCheck == this.textContainer_) {
1589
- return true;
1590
- }
1591
-
1592
- elemToCheck = elemToCheck.parentElement;
1593
- }
1594
-
1595
- return false;
1596
- }
1597
- /**
1598
- * @param {!Array<!shaka.text.Cue>} cues
1599
- * @param {!HTMLElement} container
1600
- * @param {number} currentTime
1601
- * @param {!Array<!shaka.text.Cue>} parents
1602
- * @private
1603
- */
1604
-
1605
-
1606
- updateCuesRecursive_(cues, container, currentTime, parents) {
1607
- // Set to true if the cues have changed in some way, which will require
1608
- // DOM changes. E.g. if a cue was added or removed.
1609
- let updateDOM = false;
1610
- /**
1611
- * The elements to remove from the DOM.
1612
- * Some of these elements may be added back again, if their corresponding
1613
- * cue is in toPlant.
1614
- * These elements are only removed if updateDOM is true.
1615
- * @type {!Array<!HTMLElement>}
1616
- */
1617
-
1618
- const toUproot = [];
1619
- /**
1620
- * The cues whose corresponding elements should be in the DOM.
1621
- * Some of these might be new, some might have been displayed beforehand.
1622
- * These will only be added if updateDOM is true.
1623
- * @type {!Array<!shaka.text.Cue>}
1624
- */
1625
-
1626
- const toPlant = [];
1627
-
1628
- for (const cue of cues) {
1629
- parents.push(cue);
1630
- let cueRegistry = this.currentCuesMap_.get(cue);
1631
- const shouldBeDisplayed = cue.startTime <= currentTime && cue.endTime > currentTime;
1632
- let wrapper = cueRegistry ? cueRegistry.wrapper : null;
1633
-
1634
- if (cueRegistry) {
1635
- // If the cues are replanted, all existing cues should be uprooted,
1636
- // even ones which are going to be planted again.
1637
- toUproot.push(cueRegistry.cueElement); // Also uproot all displayed region elements.
1638
-
1639
- if (cueRegistry.regionElement) {
1640
- toUproot.push(cueRegistry.regionElement);
1641
- } // If the cue should not be displayed, remove it entirely.
1642
-
1643
-
1644
- if (!shouldBeDisplayed) {
1645
- // Since something has to be removed, we will need to update the DOM.
1646
- updateDOM = true;
1647
- this.currentCuesMap_.delete(cue);
1648
- cueRegistry = null;
1649
- }
1650
- }
1651
-
1652
- if (shouldBeDisplayed) {
1653
- toPlant.push(cue);
1654
-
1655
- if (!cueRegistry) {
1656
- // The cue has to be made!
1657
- this.createCue_(cue, parents);
1658
- cueRegistry = this.currentCuesMap_.get(cue);
1659
- wrapper = cueRegistry.wrapper;
1660
- updateDOM = true;
1661
- } else if (!this.isElementUnderTextContainer_(wrapper)) {
1662
- // We found that the wrapper needs to be in the DOM
1663
- updateDOM = true;
1664
- }
1665
- } // Recursively check the nested cues, to see if they need to be added or
1666
- // removed.
1667
- // If wrapper is null, that means that the cue is not only not being
1668
- // displayed currently, it also was not removed this tick. So it's
1669
- // guaranteed that the children will neither need to be added nor removed.
1670
-
1671
-
1672
- if (cue.nestedCues.length > 0 && wrapper) {
1673
- this.updateCuesRecursive_(cue.nestedCues, wrapper, currentTime, parents);
1674
- }
1675
-
1676
- const topCue = parents.pop();
1677
- goog.asserts.assert(topCue == cue, 'Parent cues should be kept in order');
1678
- }
1679
-
1680
- if (updateDOM) {
1681
- for (const element of toUproot) {
1682
- // NOTE: Because we uproot shared region elements, too, we might hit an
1683
- // element here that has no parent because we've already processed it.
1684
- if (element.parentElement) {
1685
- element.parentElement.removeChild(element);
1686
- }
1687
- }
1688
-
1689
- toPlant.sort((a, b) => {
1690
- if (a.startTime != b.startTime) {
1691
- return a.startTime - b.startTime;
1692
- }
1693
-
1694
- return a.endTime - b.endTime;
1695
- });
1696
-
1697
- for (const cue of toPlant) {
1698
- const cueRegistry = this.currentCuesMap_.get(cue);
1699
- goog.asserts.assert(cueRegistry, 'cueRegistry should exist.');
1700
-
1701
- if (cueRegistry.regionElement) {
1702
- if (cueRegistry.regionElement.contains(container)) {
1703
- cueRegistry.regionElement.removeChild(container);
1704
- }
1705
-
1706
- container.appendChild(cueRegistry.regionElement);
1707
- cueRegistry.regionElement.appendChild(cueRegistry.cueElement);
1708
- } else {
1709
- container.appendChild(cueRegistry.cueElement);
1710
- }
1711
- }
1712
- }
1713
- }
1714
- /**
1715
- * Display the current captions.
1716
- * @param {boolean=} forceUpdate
1717
- * @private
1718
- */
1719
-
1720
-
1721
- updateCaptions_(forceUpdate = false) {
1722
- if (!this.textContainer_) {
1723
- return;
1724
- }
1725
-
1726
- const {
1727
- currentTime
1728
- } = this.video_;
1729
-
1730
- if (!this.isTextVisible_ || forceUpdate) {
1731
- // Remove child elements from all regions.
1732
- for (const regionElement of this.regionElements_.values()) {
1733
- regionElement.replaceChildren();
1734
- } // Remove all top-level elements in the text container.
1735
-
1736
-
1737
- this.textContainer_.replaceChildren(); // Clear the element maps.
1738
-
1739
- this.currentCuesMap_.clear();
1740
- this.regionElements_.clear();
1741
- }
1742
-
1743
- if (this.isTextVisible_) {
1744
-
1745
-
1746
- this.updateCuesRecursive_(this.cues_, this.textContainer_, currentTime,
1747
- /* parents= */
1748
- []);
1749
- }
1750
- }
1751
- /**
1752
- * Compute a unique internal id:
1753
- * Regions can reuse the id but have different dimensions, we need to
1754
- * consider those differences
1755
- * @param {shaka.text.CueRegion} region
1756
- * @private
1757
- */
1758
-
1759
-
1760
- generateRegionId_(region) {
1761
- const percentageUnit = shaka.text.CueRegion.units.PERCENTAGE;
1762
- const heightUnit = region.heightUnits == percentageUnit ? '%' : 'px';
1763
- const viewportAnchorUnit = region.viewportAnchorUnits == percentageUnit ? '%' : 'px';
1764
- const uniqueRegionId = `${region.id}_${region.width}x${region.height}${heightUnit}-${region.viewportAnchorX}x${region.viewportAnchorY}${viewportAnchorUnit}`;
1765
- return uniqueRegionId;
1766
- }
1767
- /**
1768
- * Get or create a region element corresponding to the cue region. These are
1769
- * cached by ID.
1770
- *
1771
- * @param {!shaka.text.Cue} cue
1772
- * @return {!HTMLElement}
1773
- * @private
1774
- */
1775
-
1776
-
1777
- getRegionElement_(cue) {
1778
- const {
1779
- region
1780
- } = cue; // from https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#caption-window-size
1781
- // if aspect ratio is 4/3, use that value, otherwise, use the 16:9 value
1782
-
1783
- const lineWidthMultiple = this.aspectRatio_ === 4 / 3 ? 2.5 : 1.9;
1784
- const lineHeightMultiple = 5.33;
1785
- const regionId = this.generateRegionId_(region);
1786
-
1787
- if (this.regionElements_.has(regionId)) {
1788
- return this.regionElements_.get(regionId);
1789
- }
1790
-
1791
- const regionElement = document.createElement('span');
1792
- const linesUnit = shaka.text.CueRegion.units.LINES;
1793
- const percentageUnit = shaka.text.CueRegion.units.PERCENTAGE;
1794
- const pixelUnit = shaka.text.CueRegion.units.PX;
1795
- let heightUnit = region.heightUnits == percentageUnit ? '%' : 'px';
1796
- let widthUnit = region.widthUnits == percentageUnit ? '%' : 'px';
1797
- const viewportAnchorUnit = region.viewportAnchorUnits == percentageUnit ? '%' : 'px';
1798
- regionElement.id = `shaka-text-region---${regionId}`;
1799
- regionElement.classList.add('shaka-text-region');
1800
- regionElement.style.position = 'absolute';
1801
- let regionHeight = region.height;
1802
- let regionWidth = region.width;
1803
-
1804
- if (region.heightUnits === linesUnit) {
1805
- regionHeight = region.height * lineHeightMultiple;
1806
- heightUnit = '%';
1807
- }
1808
-
1809
- if (region.widthUnits === linesUnit) {
1810
- regionWidth = region.width * lineWidthMultiple;
1811
- widthUnit = '%';
1812
- }
1813
-
1814
- regionElement.style.height = regionHeight + heightUnit;
1815
- regionElement.style.width = regionWidth + widthUnit;
1816
-
1817
- if (region.viewportAnchorUnits === linesUnit) {
1818
- // from https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#positioning-in-cea-708
1819
- let top = region.viewportAnchorY / 75 * 100;
1820
- const windowWidth = this.aspectRatio_ === 4 / 3 ? 160 : 210;
1821
- let left = region.viewportAnchorX / windowWidth * 100; // adjust top and left values based on the region anchor and window size
1822
-
1823
- top -= region.regionAnchorY * regionHeight / 100;
1824
- left -= region.regionAnchorX * regionWidth / 100;
1825
- regionElement.style.top = `${top}%`;
1826
- regionElement.style.left = `${left}%`;
1827
- } else {
1828
- regionElement.style.top = region.viewportAnchorY - region.regionAnchorY * regionHeight / 100 + viewportAnchorUnit;
1829
- regionElement.style.left = region.viewportAnchorX - region.regionAnchorX * regionWidth / 100 + viewportAnchorUnit;
1830
- }
1831
-
1832
- if (region.heightUnits !== pixelUnit && region.widthUnits !== pixelUnit && region.viewportAnchorUnits !== pixelUnit) {
1833
- // Clip region
1834
- const top = parseInt(regionElement.style.top.slice(0, -1), 10) || 0;
1835
- const left = parseInt(regionElement.style.left.slice(0, -1), 10) || 0;
1836
- const height = parseInt(regionElement.style.height.slice(0, -1), 10) || 0;
1837
- const width = parseInt(regionElement.style.width.slice(0, -1), 10) || 0;
1838
- const realTop = Math.max(0, Math.min(100 - height, top));
1839
- const realLeft = Math.max(0, Math.min(100 - width, left));
1840
- regionElement.style.top = `${realTop}%`;
1841
- regionElement.style.left = `${realLeft}%`;
1842
- }
1843
-
1844
- regionElement.style.display = 'flex';
1845
- regionElement.style.flexDirection = 'column';
1846
- regionElement.style.alignItems = 'center';
1847
-
1848
- if (cue.displayAlign == shaka.text.Cue.displayAlign.BEFORE) {
1849
- regionElement.style.justifyContent = 'flex-start';
1850
- } else if (cue.displayAlign == shaka.text.Cue.displayAlign.CENTER) {
1851
- regionElement.style.justifyContent = 'center';
1852
- } else {
1853
- regionElement.style.justifyContent = 'flex-end';
1854
- }
1855
-
1856
- this.regionElements_.set(regionId, regionElement);
1857
- return regionElement;
1858
- }
1859
- /**
1860
- * Creates the object for a cue.
1861
- *
1862
- * @param {!shaka.text.Cue} cue
1863
- * @param {!Array<!shaka.text.Cue>} parents
1864
- * @private
1865
- */
1866
-
1867
-
1868
- createCue_(cue, parents) {
1869
- const isNested = parents.length > 1;
1870
- let type = isNested ? 'span' : 'div';
1871
-
1872
- if (cue.lineBreak) {
1873
- type = 'br';
1874
- }
1875
-
1876
- if (cue.rubyTag) {
1877
- type = cue.rubyTag;
1878
- }
1879
-
1880
- const needWrapper = !isNested && cue.nestedCues.length > 0; // Nested cues are inline elements. Top-level cues are block elements.
1881
-
1882
- const cueElement = document.createElement(type);
1883
-
1884
- if (type != 'br') {
1885
- this.setCaptionStyles_(cueElement, cue, parents, needWrapper);
1886
-
1887
- if (!cue.line) {
1888
- cueElement.classList.add(`shaka-cue-${cue.writingMode}`);
1889
- }
1890
- }
1891
-
1892
- let regionElement = null;
1893
-
1894
- if (cue.region && cue.region.id) {
1895
- regionElement = this.getRegionElement_(cue);
1896
- }
1897
-
1898
- let wrapper = cueElement;
1899
-
1900
- if (needWrapper) {
1901
- // Create a wrapper element which will serve to contain all children into
1902
- // a single item. This ensures that nested span elements appear
1903
- // horizontally and br elements occupy no vertical space.
1904
- wrapper = document.createElement('span');
1905
- wrapper.classList.add('shaka-text-wrapper');
1906
- wrapper.style.backgroundColor = cue.backgroundColor;
1907
- wrapper.style.lineHeight = 'normal';
1908
- cueElement.appendChild(wrapper);
1909
- }
1910
-
1911
- this.currentCuesMap_.set(cue, {
1912
- cueElement,
1913
- wrapper,
1914
- regionElement
1915
- });
1916
- }
1917
- /**
1918
- * Compute cue position alignment
1919
- * See https://www.w3.org/TR/webvtt1/#webvtt-cue-position-alignment
1920
- *
1921
- * @param {!shaka.text.Cue} cue
1922
- * @private
1923
- */
1924
-
1925
-
1926
- computeCuePositionAlignment_(cue) {
1927
- const {
1928
- Cue
1929
- } = shaka.text;
1930
- const {
1931
- direction,
1932
- positionAlign,
1933
- textAlign
1934
- } = cue;
1935
-
1936
- if (positionAlign !== Cue.positionAlign.AUTO) {
1937
- // Position align is not AUTO: use it
1938
- return positionAlign;
1939
- } // Position align is AUTO: use text align to compute its value
1940
-
1941
-
1942
- if (textAlign === Cue.textAlign.LEFT || textAlign === Cue.textAlign.START && direction === Cue.direction.HORIZONTAL_LEFT_TO_RIGHT || textAlign === Cue.textAlign.END && direction === Cue.direction.HORIZONTAL_RIGHT_TO_LEFT) {
1943
- return Cue.positionAlign.LEFT;
1944
- }
1945
-
1946
- if (textAlign === Cue.textAlign.RIGHT || textAlign === Cue.textAlign.START && direction === Cue.direction.HORIZONTAL_RIGHT_TO_LEFT || textAlign === Cue.textAlign.END && direction === Cue.direction.HORIZONTAL_LEFT_TO_RIGHT) {
1947
- return Cue.positionAlign.RIGHT;
1948
- }
1949
-
1950
- return Cue.positionAlign.CENTER;
1951
- }
1952
- /**
1953
- * @param {!HTMLElement} cueElement
1954
- * @param {!shaka.text.Cue} cue
1955
- * @param {!Array<!shaka.text.Cue>} parents
1956
- * @param {boolean} hasWrapper
1957
- * @private
1958
- */
1959
-
1960
-
1961
- setCaptionStyles_(cueElement, cue, parents, hasWrapper) {
1962
- const {
1963
- Cue
1964
- } = shaka.text;
1965
-
1966
- const inherit = cb => inheritProperty_(parents, cb);
1967
-
1968
- const {
1969
- style
1970
- } = cueElement;
1971
- const isLeaf = cue.nestedCues.length == 0;
1972
- const isNested = parents.length > 1; // TODO: wrapLine is not yet supported. Lines always wrap.
1973
- // White space should be preserved if emitted by the text parser. It's the
1974
- // job of the parser to omit any whitespace that should not be displayed.
1975
- // Using 'pre-wrap' means that whitespace is preserved even at the end of
1976
- // the text, but that lines which overflow can still be broken.
1977
-
1978
- style.whiteSpace = 'pre-wrap'; // Using 'break-spaces' would be better, as it would preserve even trailing
1979
- // spaces, but that only shipped in Chrome 76. As of July 2020, Safari
1980
- // still has not implemented break-spaces, and the original Chromecast will
1981
- // never have this feature since it no longer gets firmware updates.
1982
- // So we need to replace trailing spaces with non-breaking spaces.
1983
-
1984
- const text = cue.payload.replace(/\s+$/g, match => {
1985
- const nonBreakingSpace = '\xa0';
1986
- return nonBreakingSpace.repeat(match.length);
1987
- });
1988
- style.webkitTextStrokeColor = cue.textStrokeColor;
1989
- style.webkitTextStrokeWidth = cue.textStrokeWidth;
1990
- style.color = cue.color;
1991
- style.direction = cue.direction;
1992
- style.opacity = cue.opacity;
1993
- style.paddingLeft = convertLengthValue_(cue.linePadding, cue, this.videoContainer_);
1994
- style.paddingRight = convertLengthValue_(cue.linePadding, cue, this.videoContainer_);
1995
- style.textCombineUpright = cue.textCombineUpright;
1996
- style.textShadow = cue.textShadow;
1997
-
1998
- if (cue.backgroundImage) {
1999
- style.backgroundImage = `url('${cue.backgroundImage}')`;
2000
- style.backgroundRepeat = 'no-repeat';
2001
- style.backgroundSize = 'contain';
2002
- style.backgroundPosition = 'center';
2003
-
2004
- if (cue.backgroundColor) {
2005
- style.backgroundColor = cue.backgroundColor;
2006
- } // Quoting https://www.w3.org/TR/ttml-imsc1.2/:
2007
- // "The width and height (in pixels) of the image resource referenced by
2008
- // smpte:backgroundImage SHALL be equal to the width and height expressed
2009
- // by the tts:extent attribute of the region in which the div element is
2010
- // presented".
2011
-
2012
-
2013
- style.width = '100%';
2014
- style.height = '100%';
2015
- } else {
2016
- // If we have both text and nested cues, then style everything; otherwise
2017
- // place the text in its own <span> so the background doesn't fill the
2018
- // whole region.
2019
- let elem;
2020
-
2021
- if (cue.nestedCues.length) {
2022
- elem = cueElement;
2023
- } else {
2024
- elem = document.createElement('span');
2025
- cueElement.appendChild(elem);
2026
- }
2027
-
2028
- if (cue.border) {
2029
- elem.style.border = cue.border;
2030
- }
2031
-
2032
- if (!hasWrapper) {
2033
- const bgColor = inherit(c => c.backgroundColor);
2034
-
2035
- if (bgColor) {
2036
- elem.style.backgroundColor = bgColor;
2037
- } else if (text) {
2038
- // If there is no background, default to a semi-transparent black.
2039
- // Only do this for the text itself.
2040
- elem.style.backgroundColor = 'rgba(0, 0, 0, 0)';
2041
- }
2042
- }
2043
-
2044
- if (text) {
2045
- elem.textContent = text;
2046
- }
2047
- } // The displayAlign attribute specifies the vertical alignment of the
2048
- // captions inside the text container. Before means at the top of the
2049
- // text container, and after means at the bottom.
2050
-
2051
-
2052
- if (/ruby|rt/i.test(cueElement.tagName)) ; else if (isNested && !parents[parents.length - 1].isContainer) {
2053
- style.display = 'inline';
2054
- } else {
2055
- style.display = 'flex';
2056
- style.flexDirection = 'column';
2057
- style.alignItems = 'center';
2058
-
2059
- if (cue.textAlign == Cue.textAlign.LEFT || cue.textAlign == Cue.textAlign.START) {
2060
- style.width = '100%';
2061
- style.alignItems = 'start';
2062
- } else if (cue.textAlign == Cue.textAlign.RIGHT || cue.textAlign == Cue.textAlign.END) {
2063
- style.width = '100%';
2064
- style.alignItems = 'end';
2065
- } // in VTT displayAlign can't be set, it defaults to 'after',
2066
- // but for vertical, it should be 'before'
2067
-
2068
-
2069
- if (/vertical/.test(cue.writingMode)) {
2070
- style.justifyContent = 'flex-start';
2071
- } else if (cue.displayAlign == Cue.displayAlign.BEFORE) {
2072
- style.justifyContent = 'flex-start';
2073
- } else if (cue.displayAlign == Cue.displayAlign.CENTER) {
2074
- style.justifyContent = 'center';
2075
- } else {
2076
- style.justifyContent = 'flex-end';
2077
- }
2078
- }
2079
-
2080
- if (!isLeaf) {
2081
- style.margin = '0';
2082
- }
2083
-
2084
- style.fontFamily = cue.fontFamily;
2085
- style.fontWeight = cue.fontWeight.toString();
2086
- style.fontStyle = cue.fontStyle;
2087
- style.letterSpacing = cue.letterSpacing;
2088
- style.fontSize = convertLengthValue_(cue.fontSize, cue, this.videoContainer_); // The line attribute defines the positioning of the text container inside
2089
- // the video container.
2090
- // - The line offsets the text container from the top, the right or left of
2091
- // the video viewport as defined by the writing direction.
2092
- // - The value of the line is either as a number of lines, or a percentage
2093
- // of the video viewport height or width.
2094
- // The lineAlign is an alignment for the text container's line.
2095
- // - The Start alignment means the text container’s top side (for horizontal
2096
- // cues), left side (for vertical growing right), or right side (for
2097
- // vertical growing left) is aligned at the line.
2098
- // - The Center alignment means the text container is centered at the line
2099
- // (to be implemented).
2100
- // - The End Alignment means The text container’s bottom side (for
2101
- // horizontal cues), right side (for vertical growing right), or left side
2102
- // (for vertical growing left) is aligned at the line.
2103
- // TODO: Implement line alignment with line number.
2104
- // TODO: Implement lineAlignment of 'CENTER'.
2105
-
2106
- let line = !isNested && /vertical/.test(cue.writingMode) && cue.line == null ? 0 : cue.line;
2107
-
2108
- if (line != null) {
2109
- let {
2110
- lineInterpretation
2111
- } = cue; // HACK: the current implementation of UITextDisplayer only handled
2112
- // PERCENTAGE, so we need convert LINE_NUMBER to PERCENTAGE
2113
-
2114
- if (lineInterpretation == Cue.lineInterpretation.LINE_NUMBER) {
2115
- lineInterpretation = Cue.lineInterpretation.PERCENTAGE;
2116
- let maxLines = 16; // The maximum number of lines is different if it is a vertical video.
2117
-
2118
- if (this.aspectRatio_ && this.aspectRatio_ < 1) {
2119
- maxLines = 32;
2120
- }
2121
-
2122
- if (line < 0) {
2123
- line = 100 + line / maxLines * 100;
2124
- } else {
2125
- line = line / maxLines * 100;
2126
- }
2127
- }
2128
-
2129
- if (lineInterpretation == Cue.lineInterpretation.PERCENTAGE) {
2130
- style.position = 'absolute';
2131
-
2132
- if (cue.writingMode == Cue.writingMode.HORIZONTAL_TOP_TO_BOTTOM) {
2133
- style.width = '100%';
2134
-
2135
- if (cue.lineAlign == Cue.lineAlign.START) {
2136
- style.top = `${line}%`;
2137
- } else if (cue.lineAlign == Cue.lineAlign.END) {
2138
- style.bottom = `${100 - line}%`;
2139
- }
2140
- } else if (cue.writingMode == Cue.writingMode.VERTICAL_LEFT_TO_RIGHT) {
2141
- style.height = '100%';
2142
-
2143
- if (cue.lineAlign == Cue.lineAlign.START) {
2144
- style.left = `${3 + line}%`;
2145
- } else if (cue.lineAlign == Cue.lineAlign.END) {
2146
- style.right = `${97 - line}%`;
2147
- }
2148
- } else {
2149
- style.height = '100%';
2150
-
2151
- if (cue.lineAlign == Cue.lineAlign.START) {
2152
- style.right = `${3 + line}%`;
2153
- } else if (cue.lineAlign == Cue.lineAlign.END) {
2154
- style.left = `${97 - line}%`;
2155
- }
2156
- }
2157
- }
2158
- }
2159
-
2160
- style.lineHeight = cue.lineHeight; // The positionAlign attribute is an alignment for the text container in
2161
- // the dimension of the writing direction.
2162
-
2163
- if (!isNested && /vertical/.test(cue.writingMode) && cue.position == null) {
2164
- cue.position = 1;
2165
- }
2166
-
2167
- const computedPositionAlign = this.computeCuePositionAlignment_(cue);
2168
- const computedCuePosition = !isNested && /vertical/.test(cue.writingMode) && cue.position == null ? 1 : cue.position;
2169
-
2170
- if (/ruby|rt/i.test(cueElement.tagName)) ; else if (computedPositionAlign == Cue.positionAlign.LEFT) {
2171
- style.cssFloat = 'left';
2172
-
2173
- if (computedCuePosition !== null) {
2174
- style.position = 'absolute';
2175
-
2176
- if (cue.writingMode == Cue.writingMode.HORIZONTAL_TOP_TO_BOTTOM) {
2177
- style.left = `${computedCuePosition}%`;
2178
- style.width = 'auto';
2179
- } else {
2180
- style.top = `${computedCuePosition}%`;
2181
- }
2182
- }
2183
- } else if (computedPositionAlign == Cue.positionAlign.RIGHT) {
2184
- style.cssFloat = 'right';
2185
-
2186
- if (computedCuePosition !== null) {
2187
- style.position = 'absolute';
2188
-
2189
- if (cue.writingMode == Cue.writingMode.HORIZONTAL_TOP_TO_BOTTOM) {
2190
- style.right = `${100 - computedCuePosition}%`;
2191
- style.width = 'auto';
2192
- } else {
2193
- style.bottom = `${computedCuePosition}%`;
2194
- }
2195
- }
2196
- } else if (computedCuePosition !== null && computedCuePosition != 50) {
2197
- style.position = 'absolute';
2198
-
2199
- if (cue.writingMode == Cue.writingMode.HORIZONTAL_TOP_TO_BOTTOM) {
2200
- style.left = `${computedCuePosition}%`;
2201
- style.width = 'auto';
2202
- } else {
2203
- style.top = `${computedCuePosition}%`;
2204
- }
2205
- }
2206
-
2207
- style.textAlign = cue.textAlign;
2208
- style.textDecoration = cue.textDecoration.join(' ');
2209
- style.writingMode = cue.writingMode; // Old versions of Chromium, which may be found in certain versions of Tizen
2210
- // and WebOS, may require the prefixed version: webkitWritingMode.
2211
- // https://caniuse.com/css-writing-mode
2212
- // However, testing shows that Tizen 3, at least, has a 'writingMode'
2213
- // property, but the setter for it does nothing. Therefore we need to
2214
- // detect that and fall back to the prefixed version in this case, too.
2215
-
2216
- if (!('writingMode' in document.documentElement.style) || style.writingMode != cue.writingMode) {
2217
- // Note that here we do not bother to check for webkitWritingMode support
2218
- // explicitly. We try the unprefixed version, then fall back to the
2219
- // prefixed version unconditionally.
2220
- style.webkitWritingMode = cue.writingMode;
2221
- } // The size is a number giving the size of the text container, to be
2222
- // interpreted as a percentage of the video, as defined by the writing
2223
- // direction.
2224
-
2225
-
2226
- if (cue.size) {
2227
- if (cue.writingMode != Cue.writingMode.HORIZONTAL_TOP_TO_BOTTOM) {
2228
- style.height = `${cue.size}%`;
2229
- }
2230
- }
2231
- }
2232
-
2233
- }
2234
-
2235
- /* eslint-disable eqeqeq */
2236
-
2237
- const getQualityItem = track => ({
2238
- id: track.originalVideoId,
2239
- bitrate: track.videoBandwidth,
2240
- width: track.width,
2241
- height: track.height,
2242
- codec: track.videoCodec,
2243
- frameRate: track.frameRate
2244
- });
2245
-
2246
- const rewriteDashManifest = (type, response) => {
2247
- const {
2248
- MANIFEST
2249
- } = window.shaka.net.NetworkingEngine.RequestType;
2250
-
2251
- if (type !== MANIFEST) {
2252
- return response;
2253
- }
2254
-
2255
- const data = new TextDecoder().decode(response.data);
2256
-
2257
- if (/<MPD/i.test(data)) {
2258
- response.data = new TextEncoder().encode(fixDashManifest(data));
2259
- }
2260
-
2261
- return response;
2262
- };
2263
-
2264
- const sleep = ms => new Promise(r => {
2265
- setTimeout(r, ms);
2266
- });
2267
-
2268
- const MAX_ERROR_RETRY = 3;
2269
- const RETRY_DELAY = 5000; // waiting for media server (shaka-package) to reset
2270
-
2271
- const isSegmentNotFound = detail => (detail === null || detail === void 0 ? void 0 : detail.code) === window.shaka.util.Error.Code.BAD_HTTP_STATUS && detail.severity === window.shaka.util.Error.Severity.RECOVERABLE && (detail === null || detail === void 0 ? void 0 : detail.data[1]) === 404;
2272
-
2273
- const syncStartOver = (media, player) => {
2274
- media.addEventListener('timeupdate', () => {
2275
- const {
2276
- start,
2277
- end
2278
- } = player.seekRange();
2279
- const defaultLiveOffset = end - Date.now() / 1000;
2280
- const seekDurationDiff = end - start - Date.now() / 1000;
2281
-
2282
- if (media.defaultLiveOffset && Math.abs(defaultLiveOffset - media.defaultLiveOffset) + Math.abs(seekDurationDiff - media.seekDurationDiff) > 7) {
2283
- media.defaultLiveOffset = defaultLiveOffset;
2284
- media.seekDurationDiff = seekDurationDiff;
2285
- }
2286
- });
2287
- }; // TODO: we avoid depending on base player instance, but playlog things should be in playlog place
2288
-
2289
-
2290
- const proxyTextTrackEvents = (media, {
2291
- player
2292
- }) => {
2293
- const handleTextTrackChange = () => media.dispatchEvent(new CustomEvent('textTrackChange', {
2294
- detail: {
2295
- getTextTracks: () => player.isTextTrackVisible() ? player.getTextTracks() : []
2296
- }
2297
- }));
2298
-
2299
- player.addEventListener('texttrackvisibility', handleTextTrackChange);
2300
- player.addEventListener('textchange', handleTextTrackChange);
2301
- };
2302
-
2303
- const loadShaka = async (videoElement, config = {}, options = {}) => {
2304
- window.shakaMediaKeysPolyfill = true;
2305
- const shakaModule = await import('shaka-player');
2306
- const shaka = shakaModule.default || shakaModule;
2307
- window.shaka = shaka;
2308
- shaka.polyfill.installAll();
2309
- const player = new shaka.Player();
2310
- getUrlObject(mediaSource => {
2311
- if (player) {
2312
- player.mediaSource = mediaSource;
2313
- }
2314
- });
2315
- player.attach(videoElement);
2316
- player.configure({ ...config,
2317
- manifest: {
2318
- dash: {
2319
- ignoreSuggestedPresentationDelay: true
2320
- },
2321
- retryParameters: {
2322
- maxAttempts: 6
2323
- },
2324
- ...config.manifest
2325
- },
2326
- streaming: {
2327
- autoLowLatencyMode: true,
2328
- // To reduce the unseekable range at the start of the manifests.
2329
- // See: https://github.com/shaka-project/shaka-player/issues/3526
2330
- safeSeekOffset: 0,
2331
- rebufferingGoal: 0,
2332
- ...(isSafari() && {
2333
- preferNativeHls: true
2334
- }),
2335
- ...config.streaming
2336
- },
2337
- // iPhone Safari is video only fullscreen must use native text display
2338
- ...(options.container && document.fullscreenEnabled && {
2339
- textDisplayFactory: () => new UITextDisplayer(videoElement, options.container)
2340
- })
2341
- });
2342
- syncStartOver(videoElement, player);
2343
- player.addEventListener('error', event => {
2344
- var _detail$data, _detail$data$some, _detail$message, _window$Sentry;
2345
-
2346
- console.log(event);
2347
- const {
2348
- detail = {}
2349
- } = event;
2350
- const error = new Error(`Player: ${detail.code}/${detail.name}`);
2351
-
2352
- if (!detail || /The video element has thrown a media error|Video element triggered an Error/.test(detail.message) || (_detail$data = detail.data) !== null && _detail$data !== void 0 && (_detail$data$some = _detail$data.some) !== null && _detail$data$some !== void 0 && _detail$data$some.call(_detail$data, message => /Unsupported source type/i.test(message))) {
2353
- return;
2354
- }
2355
-
2356
- if (detail.code == 3016) {
2357
- return;
2358
- } // Handle D3 edge case: we don't fetch segments due to wrong index.
2359
-
2360
-
2361
- if (isSegmentNotFound(detail)) {
2362
- setTimeout(() => {
2363
- if (player.errorRetry < MAX_ERROR_RETRY) {
2364
- player.errorRetry += 1;
2365
- player.reload();
2366
- }
2367
- }, RETRY_DELAY);
2368
- return;
2369
- }
2370
-
2371
- videoElement.dispatchEvent(Object.assign(new CustomEvent('error'), {
2372
- error: detail,
2373
- message: `Player Error: ${detail.code}/${(_detail$message = detail.message) === null || _detail$message === void 0 ? void 0 : _detail$message.split(' ', 3)[2]}`
2374
- }));
2375
-
2376
- if (detail.code === 1001 || detail.severity === 2) {
2377
- console.info('Stream unavailable, unload source');
2378
- player.unload();
2379
- }
2380
-
2381
- (_window$Sentry = window.Sentry) === null || _window$Sentry === void 0 ? void 0 : _window$Sentry.captureException(error);
2382
- });
2383
- player.addEventListener('loaded', () => {
2384
- videoElement.dispatchEvent(new CustomEvent('canplay', {}));
2385
- });
2386
- player.addEventListener('adaptation', event => {
2387
- const {
2388
- videoBandwidth,
2389
- width,
2390
- height
2391
- } = event.newTrack;
2392
-
2393
- if (event.oldTrack.height !== height) {
2394
- videoElement.dispatchEvent(new CustomEvent('downloadQualityChange', {
2395
- detail: {
2396
- bitrate: parseInt(videoBandwidth / 1000, 10),
2397
- height,
2398
- width
2399
- }
2400
- }));
2401
- }
2402
- });
2403
- player.addEventListener('variantchanged', event => {
2404
- const {
2405
- language
2406
- } = event.newTrack;
2407
-
2408
- if (event.oldTrack.language !== language) {
2409
- videoElement.dispatchEvent(new CustomEvent('audioTrackChange', {
2410
- detail: {
2411
- lang: language
2412
- }
2413
- }));
2414
- }
2415
- });
2416
- proxyTextTrackEvents(videoElement, {
2417
- player
2418
- });
2419
- const extensionOptions = {
2420
- requestHandlers: [],
2421
- responseHandlers: [rewriteDashManifest]
2422
- };
2423
-
2424
- if (isSafari()) {
2425
- setupKKFariplay(player, extensionOptions);
2426
- }
2427
-
2428
- const getAvailableVideoQualities = () => player.getVariantTracks().reduce((trackList, currentTrack) => {
2429
- const keepOrignalTrack = trackList.find(track => track.height === currentTrack.height);
2430
-
2431
- if (!keepOrignalTrack) {
2432
- trackList.push(getQualityItem(currentTrack));
2433
- }
2434
-
2435
- return trackList;
2436
- }, []);
2437
-
2438
- const getVideoQuality = () => {
2439
- const activeTrack = player.getVariantTracks().find(track => track.active);
2440
- if (!activeTrack) return {};
2441
- return getQualityItem(activeTrack);
2442
- };
2443
-
2444
- VttTextParser.register(shaka);
2445
- HttpFetchPlugin.register(shaka);
2446
- const networkEngine = player.getNetworkingEngine();
2447
- networkEngine.registerRequestFilter((type, request) => extensionOptions.requestHandlers.reduce((merged, handler) => handler(type, merged, player, extensionOptions), request));
2448
- networkEngine.registerResponseFilter((type, response) => extensionOptions.responseHandlers.reduce((merged, handler) => handler(type, merged, player, extensionOptions), response));
2449
- const load = player.load.bind(player); // refactor suggestion: don't add things to extensions unless there's no other way,
2450
- // if something needs to be changed, extract it first.
2451
-
2452
- const extensions = {
2453
- shaka,
2454
- errorRetry: 0,
2455
- lastLoad: {},
2456
-
2457
- get mediaSource() {
2458
- return player.mediaSource;
2459
- },
2460
-
2461
- configureExtensions: ({
2462
- drm
2463
- } = {}) => {
2464
- extensionOptions.drm = drm;
2465
- },
2466
- load: async (assetUri, startTime, mimeType) => {
2467
- await sleep(0);
2468
- player.lastLoad = {
2469
- assetUri,
2470
- startTime,
2471
- mimeType
2472
- };
2473
- return load(assetUri, startTime, mimeType);
2474
- },
2475
- reload: async () => {
2476
- await player.unload();
2477
- const {
2478
- assetUri,
2479
- startTime,
2480
- mimeType
2481
- } = player.lastLoad;
2482
- return player.load(assetUri, startTime, mimeType);
2483
- },
2484
- getPlaybackSpeed: () => videoElement.playbackRate,
2485
- getVideoElement: () => videoElement,
2486
- getVideoQuality,
2487
- getAvailableVideoQualities,
2488
- isAlive: () => player.getLoadMode() !== shaka.Player.LoadMode.DESTROYED,
2489
- on: player.addEventListener.bind(player)
2490
- };
2491
- Object.assign(player, extensions);
2492
- return player;
2493
- };
2494
-
2495
- const protocolExtensions = {
2496
- hls: 'm3u8',
2497
- dash: 'mpd'
2498
- };
2499
- const mimeTypes = {
2500
- hls: 'application/x-mpegurl',
2501
- dash: 'application/dash+xml'
2502
- };
2503
-
2504
- const matchType = (source, manifestType) => {
2505
- var _source$type, _source$type2, _ref, _ref$endsWith;
2506
-
2507
- return ((_source$type = source.type) === null || _source$type === void 0 ? void 0 : _source$type.toLowerCase().includes(manifestType)) || ((_source$type2 = source.type) === null || _source$type2 === void 0 ? void 0 : _source$type2.toLowerCase()) === mimeTypes[manifestType] || ((_ref = source.src || source) === null || _ref === void 0 ? void 0 : (_ref$endsWith = _ref.endsWith) === null || _ref$endsWith === void 0 ? void 0 : _ref$endsWith.call(_ref, protocolExtensions[manifestType]));
2508
- };
2509
-
2510
- const getDrmOptions$1 = fallbackDrm => {
2511
- if (!(fallbackDrm !== null && fallbackDrm !== void 0 && fallbackDrm.url)) {
2512
- return;
2513
- }
2514
-
2515
- const drmOptions = {
2516
- licenseUri: fallbackDrm.url,
2517
- headers: fallbackDrm.headers
2518
- };
2519
- return {
2520
- widevine: drmOptions,
2521
- fairplay: { ...drmOptions,
2522
- certificateUri: `${fallbackDrm.url}/fairplay_cert`,
2523
- ...fallbackDrm.fairplay
2524
- },
2525
- playready: drmOptions
2526
- };
2527
- };
2528
- /**
2529
- * @typedef {{src: string, type: string}} SourceObject
2530
- * @typedef {{hls: string, dash: string}} SourceObjectAlt backward compatiable form
2531
- *
2532
- * @param {SourceObject[]|SourceObject|SourceObjectAlt|string} sourceOptions
2533
- * @param {{preferManifestType?: ('dash'|'hls'|'platform')}} options
2534
- * @return {{src: string, type: string, drm: Object}}
2535
- */
2536
-
2537
-
2538
- const getSource = (sourceOptions, {
2539
- preferManifestType,
2540
- fallbackDrm
2541
- } = {}) => {
2542
- if (sourceOptions.dash || sourceOptions.hls) {
2543
- const {
2544
- dash,
2545
- hls
2546
- } = sourceOptions;
2547
- return getSource([hls && {
2548
- src: hls,
2549
- type: mimeTypes.hls
2550
- }, dash && {
2551
- src: dash,
2552
- type: mimeTypes.dash
2553
- }].filter(Boolean), {
2554
- preferManifestType,
2555
- fallbackDrm
2556
- });
2557
- }
2558
-
2559
- if (!Array.isArray(sourceOptions)) {
2560
- return getSource([sourceOptions], {
2561
- preferManifestType,
2562
- fallbackDrm
2563
- });
2564
- }
2565
-
2566
- if (fallbackDrm) {
2567
- return getSource(sourceOptions.map(option => ({ ...(option.src ? option : {
2568
- src: option
2569
- }),
2570
- drm: getDrmOptions$1(fallbackDrm)
2571
- })), {
2572
- preferManifestType
2573
- });
2574
- }
2575
-
2576
- const targetType = preferManifestType !== 'platform' ? preferManifestType : isSafari() ? 'hls' : 'dash';
2577
- const matched = sourceOptions.find(source => matchType(source, targetType));
2578
- const selected = matched || sourceOptions[0];
2579
-
2580
- if (!selected) {
2581
- return;
2582
- }
2583
-
2584
- const type = matched && preferManifestType === 'hls' && mimeTypes.hls;
2585
- return { ...(selected.src ? selected : {
2586
- src: selected
2587
- }),
2588
- type
2589
- };
2590
- };
2591
-
2592
- const getSourceText = source => [].concat(source).filter(item => item.type === 'text/vtt');
2593
-
2594
- /* eslint-disable no-param-reassign */
2595
- const keySystems = {
2596
- widevine: 'com.widevine.alpha',
2597
- fairplay: 'com.apple.fps.1_0',
2598
- playready: 'com.microsoft.playready'
2599
- }; // TODO widevine levels
2600
-
2601
- const getDrmOptions = source => {
2602
- const drm = source.drm && Object.entries(source.drm).reduce((result, [keySystemId, options]) => {
2603
- const uri = typeof options === 'string' ? options : options.licenseUri;
2604
-
2605
- if (uri) {
2606
- const keySystemName = keySystems[keySystemId] || keySystemId;
2607
- result.servers[keySystemName] = uri;
2608
- const {
2609
- headers,
2610
- certificateUri
2611
- } = options;
2612
- const advanced = [headers && {
2613
- headers
2614
- }, certificateUri && {
2615
- serverCertificateUri: certificateUri
2616
- }].filter(Boolean);
2617
-
2618
- if (advanced.length > 0) {
2619
- result.advanced[keySystemName] = Object.assign({}, ...advanced);
2620
- }
2621
- }
2622
-
2623
- return result;
2624
- }, {
2625
- servers: {},
2626
- advanced: {}
2627
- });
2628
- const extensions = source.drm && Object.entries(source.drm).reduce((result, [keySystemId, options]) => {
2629
- const keySystemName = keySystems[keySystemId] || keySystemId;
2630
-
2631
- if (options.headers || options.certificateHeaders) {
2632
- result[keySystemName] = {
2633
- headers: options.headers,
2634
- ...(options.certificateHeaders && {
2635
- certificateHeaders: options.certificateHeaders
2636
- })
2637
- };
2638
- }
2639
-
2640
- return result;
2641
- }, {});
2642
- return [drm, {
2643
- drm: extensions
2644
- }];
2645
- };
2646
-
2647
- /* eslint-disable no-param-reassign */
2648
- // when the gap is small enough, we consider it is on edge.
2649
- // The magic number 10s comes from observation of YouTube
2650
-
2651
- const LIVE_EDGE_GAP = 10; // In shaka, duration is always 2^32 in live.
2652
-
2653
- const SHAKA_LIVE_DURATION = 4294967296;
2654
-
2655
- const isLiveDuration = duration => duration >= SHAKA_LIVE_DURATION;
2656
-
2657
- const isEnded = media => !isLiveDuration(media.initialDuration) && media.initialDuration - media.currentTime < 1; // When donwload bandwidth is low, Safari may report time update while buffering, ignore it.
2658
-
2659
-
2660
- const isBuffered = media => isSafari() || Array.from({
2661
- length: media.buffered.length
2662
- }, (_, index) => ({
2663
- start: media.buffered.start(index),
2664
- end: media.buffered.end(index)
2665
- })).some( // in Safari buffered is clipped to integer
2666
- range => range.start <= media.currentTime && media.currentTime <= range.end + 1);
2667
-
2668
- const getLiveTime = (media, {
2669
- player
2670
- }) => {
2671
- const now = Date.now() / 1000;
2672
- const currentOffset = media.currentTime - media.defaultLiveOffset - now;
2673
- const seekDuration = media.seekDurationDiff + now;
2674
- const {
2675
- start,
2676
- end
2677
- } = player.seekRange();
2678
- return {
2679
- streamType: 'live',
2680
- startTime: -seekDuration,
2681
- currentTime: currentOffset < -LIVE_EDGE_GAP ? currentOffset : 0,
2682
- duration: end - start > 5 * LIVE_EDGE_GAP ? seekDuration : 0
2683
- };
2684
- };
2685
-
2686
- const getMediaTime = (media, {
2687
- player,
2688
- plugins = []
2689
- }) => {
2690
- const {
2691
- duration,
2692
- ...data
2693
- } = Object.assign(isLiveDuration(media.initialDuration) ? getLiveTime(media, {
2694
- player,
2695
- plugins
2696
- }) : {
2697
- currentTime: media.currentTime,
2698
- bufferTime: Math.max(...Array.from({
2699
- length: media.buffered.length
2700
- }, (_, index) => media.buffered.end(index))),
2701
- duration: media.initialDuration // monkey patched, duration may change for DASH playback
2702
-
2703
- }, ...plugins.map(plugin => {
2704
- var _plugin$getPlaybackSt;
2705
-
2706
- return (_plugin$getPlaybackSt = plugin.getPlaybackStatus) === null || _plugin$getPlaybackSt === void 0 ? void 0 : _plugin$getPlaybackSt.call(plugin);
2707
- }));
2708
- return { ...data,
2709
- ...((isLiveDuration(media.initialDuration) || Math.abs(media.duration - media.initialDuration) < 0.5) && {
2710
- duration
2711
- })
2712
- };
2713
- };
2714
-
2715
- const HAVE_METADATA = 1;
2716
-
2717
- const getCurrentPlaybackState = media => media.autoplay ? 'playing' : 'paused';
2718
-
2719
- const subscribePlaybackState = (media, updateState, {
2720
- iOSFullscreenDetectThreshold = 500
2721
- } = {}) => {
2722
- const lastUpdate = {
2723
- state: '',
2724
- time: 0
2725
- };
2726
-
2727
- const updateIfChanged = (event, state) => {
2728
- // Safari may report abnormal currentTime, safe to ignore
2729
- if (media.currentTime > 1e13) {
2730
- return;
2731
- }
2732
-
2733
- lastUpdate.time = media.currentTime;
2734
- lastUpdate.eventTime = Date.now();
2735
- lastUpdate.eventType = event.type;
2736
-
2737
- if (state !== lastUpdate.state) {
2738
- lastUpdate.state = state; // don't sync while in iOS/iPhone video only fullscreen, until exit
2739
-
2740
- lastUpdate.batched = media.webkitDisplayingFullscreen;
2741
-
2742
- if (!lastUpdate.batched) {
2743
- updateState(event, state);
2744
- }
2745
- }
2746
- };
2747
-
2748
- const updateBufferingState = event => {
2749
- if (!media.paused && !media.ended) {
2750
- updateIfChanged(event, 'buffering');
2751
- }
2752
- };
2753
-
2754
- const updatePlaybackTime = event => {
2755
- if (!media.paused && isBuffered(media) && media.currentTime - lastUpdate.time > 0.01) {
2756
- updateIfChanged(event, 'playing');
2757
- }
2758
- };
2759
-
2760
- const updateEnd = event => {
2761
- if (event.type === 'emptied' && lastUpdate.state !== 'loading') {
2762
- updateIfChanged(event, 'emptied');
2763
- return true;
2764
- }
2765
-
2766
- if (isEnded(media) || event.type === 'ended') {
2767
- updateIfChanged(event, 'ended');
2768
- return true;
2769
- }
2770
- };
2771
-
2772
- const registered = [on(media, 'error', event => updateIfChanged(event, 'error')), on(media, 'waiting', updateBufferingState), on(media, 'loadsource', event => updateIfChanged(event, 'loading')), on(media, 'loadedmetadata', event => setTimeout(() => {
2773
- if (media.paused && !(media.readyState > HAVE_METADATA)) {
2774
- updateIfChanged(event, getCurrentPlaybackState(media));
2775
- }
2776
- }, 1500)), on(media, 'loadeddata', event => setTimeout(() => {
2777
- if (media.paused && !event.defaultPrevented) {
2778
- updateIfChanged(event, getCurrentPlaybackState(media));
2779
- }
2780
- }, 1)), on(media, 'canplay', event => {
2781
- if (media.paused && (lastUpdate.state !== 'loading' || isIOS())) {
2782
- updateIfChanged(event, getCurrentPlaybackState(media));
2783
- }
2784
-
2785
- updatePlaybackTime(event);
2786
- }), on(media, 'pause', event => {
2787
- // TODO extract iOS fullscreen handling
2788
- // there's 1 system triggered pause, ignore once to keep playing
2789
- if (Date.now() - lastUpdate.endFullscreenAt < 5 * iOSFullscreenDetectThreshold) {
2790
- lastUpdate.endFullscreenAt = 0;
2791
- media.play();
2792
- console.debug('Ignore pause from iOS exit fullscreen');
2793
- return;
2794
- }
2795
-
2796
- if (!updateEnd(event)) {
2797
- updateIfChanged(event, 'paused');
2798
- }
2799
- }), on(media, 'seeking', updateBufferingState), on(media, 'seeked', event => {
2800
- if (lastUpdate.state === 'loading') {
2801
- updateIfChanged(event, getCurrentPlaybackState(media));
2802
- }
2803
- }), on(media, 'timeupdate', updatePlaybackTime), on(media, 'ended', updateEnd), on(media, 'emptied', updateEnd), on(media, 'webkitendfullscreen', event => {
2804
- // paused by native exit fullscreen button, should resume playing
2805
- if (lastUpdate.state === 'paused' && Date.now() - lastUpdate.eventTime < iOSFullscreenDetectThreshold) {
2806
- lastUpdate.endFullscreenAt = Date.now();
2807
- waitFor(() => !media.webkitDisplayingFullscreen, () => updateState(event, 'playing'));
2808
- } else {
2809
- // sync back current playback state in fullscreen
2810
- updateState(event, lastUpdate.state);
2811
- }
2812
- })];
2813
- return () => registered.forEach(off => off());
2814
- };
2815
-
2816
- const seek = async (media, {
2817
- player,
2818
- plugins = []
2819
- }, time, issuer) => {
2820
- if (media.readyState < HAVE_METADATA) {
2821
- await new Promise(resolve => {
2822
- media.addEventListener('loadeddata', resolve, {
2823
- once: true
2824
- });
2825
- });
2826
- } // TODO skip seeking to too near point, consider SSAI cases
2827
-
2828
-
2829
- const seekPlugin = plugins.find(plugin => typeof plugin.handleSeek === 'function' && plugin.isActive());
2830
-
2831
- const seekInternal = seekTime => {
2832
- var _player$isLive, _player$seek;
2833
-
2834
- // seeking to end video may cause Shaka glich, so move back a little
2835
- if (seekTime <= media.duration + 7 && seekTime >= media.duration - 1.0) {
2836
- return seekInternal(media.duration - 1.1);
2837
- }
2838
-
2839
- player.shouldPlayFromEdge = false;
2840
- 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
2841
-
2842
- (_player$seek = player.seek) === null || _player$seek === void 0 ? void 0 : _player$seek.call(player, seekTime, issuer); // player.seek sets time after adding segments,
2843
- // set again to reflect instantly
2844
-
2845
- if (Math.abs(seekTime) <= LIVE_EDGE_GAP && seekOrigin !== 0) {
2846
- player.goToLive();
2847
- } else {
2848
- media.currentTime = seekTime + seekOrigin;
2849
- }
2850
-
2851
- once(media, 'seeked', () => {
2852
- // when seeking to the end it may result in a few seconds earlier
2853
- if (Math.abs(seekTime + seekOrigin - media.currentTime) > 0.5) {
2854
- media.currentTime = seekTime + seekOrigin;
2855
- }
2856
- });
2857
- };
2858
-
2859
- if (seekPlugin) {
2860
- seekPlugin.handleSeek(time, seekInternal);
2861
- } else {
2862
- seekInternal(time);
2863
- }
2864
- };
2865
-
2866
- const load = async (media, {
2867
- player,
2868
- startTime,
2869
- plugins = []
2870
- }, source) => {
2871
- const preferred = getSource(source, {
2872
- preferManifestType: 'platform'
2873
- }); // There's no use case that changing DRM options without changing manifest URL, just skip
2874
-
2875
- if (player.lastSrc === (preferred === null || preferred === void 0 ? void 0 : preferred.src)) {
2876
- console.info('src is unchanged, skip load', preferred.src);
2877
- return;
2878
- }
2879
-
2880
- player.lastSrc = preferred === null || preferred === void 0 ? void 0 : preferred.src; // playlog v2 depends on this event
2881
-
2882
- media.dispatchEvent(new CustomEvent('loadsource'));
2883
- const merged = await plugins.reduce(async (loadChain, plugin) => {
2884
- var _plugin$load;
2885
-
2886
- const currentSource = await loadChain;
2887
- const overrides = await ((_plugin$load = plugin.load) === null || _plugin$load === void 0 ? void 0 : _plugin$load.call(plugin, currentSource, {
2888
- video: media,
2889
- player,
2890
- source: currentSource,
2891
- startTime,
2892
- streamFormat: source.type,
2893
- reload: async () => {
2894
- // Bitmovin unexpectedly restores muted state, so save to restore
2895
- const restoreMuted = player.isMuted && {
2896
- muted: player.isMuted()
2897
- };
2898
- player.lastSrc = '';
2899
- await load(media, {
2900
- player,
2901
- startTime,
2902
- plugins
2903
- }, source);
2904
-
2905
- if (restoreMuted) {
2906
- player[restoreMuted.muted ? 'mute' : 'unmute']();
2907
- }
2908
- }
2909
- }));
2910
- return overrides ? { ...currentSource,
2911
- ...(overrides.url && {
2912
- src: overrides.url
2913
- }),
2914
- ...(overrides.startTime >= 0 && {
2915
- startTime: overrides.startTime
2916
- })
2917
- } : currentSource;
2918
- }, { ...preferred,
2919
- startTime
2920
- });
2921
- media.addEventListener('durationchange', () => {
2922
- // media duration may change when playing VOD to live or SSAI streams, save it here for convenience
2923
- media.initialDuration = media.duration;
2924
- }, {
2925
- once: true
2926
- });
2927
- once(media, 'loadeddata', () => {
2928
- const seekToStart = (delay = 1) => {
2929
- if (merged.startTime > 0 || merged.startTime < 0) {
2930
- // Safari may glitch if seek immediately, so wait a little bit
2931
- setTimeout(() => seek(media, {
2932
- player,
2933
- plugins
2934
- }, merged.startTime), delay);
2935
- }
2936
- };
2937
-
2938
- if (player.isLive()) {
2939
- player.shouldPlayFromEdge = player.isLive() && !(merged.startTime < 0);
2940
- once(media, 'timeupdate', () => {
2941
- player.shouldPlayFromEdge = false;
2942
- const {
2943
- start,
2944
- end
2945
- } = player.seekRange();
2946
- media.defaultLiveOffset = media.currentTime - Date.now() / 1000;
2947
- media.seekDurationDiff = end - start - Date.now() / 1000;
2948
- seekToStart();
2949
- });
2950
- } else {
2951
- seekToStart();
2952
- }
2953
- });
2954
- const [drmOptions, extensions] = getDrmOptions(preferred);
2955
- player.configure({
2956
- drm: drmOptions
2957
- });
2958
- player.configureExtensions(extensions);
2959
- let loadStartTime; // TODO unset or find a proper start time for live streams
2960
-
2961
- if (merged.type !== 'application/x-mpegurl') {
2962
- loadStartTime = merged.startTime;
2963
- }
2964
-
2965
- return player.unload().then(() => player.load(merged.src, loadStartTime, merged.type)).then(loadResult => {
2966
- getSourceText(source).forEach(({
2967
- src,
2968
- language = 'en',
2969
- type = 'text/vtt',
2970
- label = language
2971
- }) => player.addTextTrackAsync(src, language, 'subtitles', type, '', label).catch(error => console.warn('Failed to add text track', error)));
2972
- return loadResult;
2973
- }).catch(error => {
2974
- media.dispatchEvent(Object.assign(new CustomEvent('error'), {
2975
- error
2976
- }));
2977
- });
2978
- };
2979
-
2980
- const reloadOnLiveStall = (media, {
2981
- reload,
2982
- stallDuration = 6000
2983
- }) => {
2984
- const getBufferedEnd = () => {
2985
- var _media$buffered;
2986
-
2987
- return Math.max(0, ...Array.from({
2988
- length: ((_media$buffered = media.buffered) === null || _media$buffered === void 0 ? void 0 : _media$buffered.length) || 0
2989
- }, (_, i) => media.buffered.end(i)));
2990
- };
2991
-
2992
- let checkTimer;
2993
- const state = {
2994
- lastBufferedEnd: 0
2995
- };
2996
- const removeListener = on(media, 'loadeddata', () => {
2997
- if (!isLiveDuration(media.duration)) {
2998
- return;
2999
- }
3000
-
3001
- const checkStall = () => {
3002
- const currentBufferedEnd = getBufferedEnd();
3003
-
3004
- if (!media.paused && state.lastBufferedEnd === currentBufferedEnd && ( // playback position may move to edge on SegmentTemplate manifest update,
3005
- // in this case buffer is empty
3006
- media.buffered.length < 1 || state.lastPlayed === media.currentTime)) {
3007
- console.warn('Live stream stall, reload to recover');
3008
- state.lastBufferedEnd = 0;
3009
- reload();
3010
- }
3011
-
3012
- state.lastBufferedEnd = currentBufferedEnd;
3013
- state.lastPlayed = media.currentTime;
3014
- clearTimeout(checkTimer);
3015
- checkTimer = setTimeout(checkStall, stallDuration);
3016
- };
3017
-
3018
- clearTimeout(checkTimer);
3019
- checkTimer = setTimeout(checkStall, stallDuration);
3020
- });
3021
- return () => {
3022
- removeListener();
3023
- clearTimeout(checkTimer);
3024
- };
3025
- };
3026
-
3027
- const loadPlayer = async (videoElement, {
3028
- container,
3029
- source,
3030
- shaka,
3031
- bitmovin
3032
- }) => {
3033
- // TODO unsubscribe to release the video element
3034
- // when resuming from background, video may play without no media events
3035
- // on some iOS devices, pause again to workaround
3036
- if (isIOS()) {
3037
- on(document, 'visibilitychange', () => setTimeout(() => videoElement.pause(), 50));
3038
- }
3039
-
3040
- printVersion();
3041
- let player;
3042
-
3043
- if (source !== null && source !== void 0 && source.native) {
3044
- player = await loadNative({
3045
- videoElement
3046
- });
3047
- } // default to Shaka
3048
- else if (shaka || !bitmovin) {
3049
- player = await loadShaka(videoElement, shaka, {
3050
- container
3051
- });
3052
- videoElement.dispatchEvent(new CustomEvent('playerStarted'));
3053
- } // use external load function to avoid Bitmovin dependency, TODO docs to be added
3054
- else if (bitmovin !== null && bitmovin !== void 0 && bitmovin.load) {
3055
- player = await bitmovin.load({
3056
- container,
3057
- videoElement,
3058
- config: bitmovin
3059
- });
3060
- videoElement.dispatchEvent(new CustomEvent('playerStarted'));
3061
- } // TODO load other players: dash.js, hls.js
3062
-
3063
-
3064
- reloadOnLiveStall(videoElement, {
3065
- reload: () => player.reload()
3066
- });
3067
- player.preferredSettings = {}; // TODO load built-in modules here
3068
-
3069
- player.modules = {};
3070
- return player;
3071
- };
3072
-
3073
- var loadPlayer$1 = loadPlayer;
3074
-
3075
- export { getMediaTime, isLiveDuration, load, loadPlayer$1 as loadPlayer, seek, subscribePlaybackState };