@kkcompany/player 2.25.0-canary.24 → 2.25.0-canary.25
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/CHANGELOG.md +2 -0
- package/dist/StallReload-BFlQRphx.mjs +717 -0
- package/dist/Video-CMbK-cxg.mjs +120 -0
- package/dist/adaptation-BcTsh-wx.mjs +74 -0
- package/dist/api-2BOrEA5d.mjs +1057 -0
- package/dist/debugUtil-IF7p5TSI.mjs +23 -0
- package/dist/events-B3vI3Srm.mjs +16 -0
- package/dist/fixDashManifest-CJ63KKaA.mjs +56 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +3 -10153
- package/dist/loadPlayer-CQdGA3Te.mjs +1560 -0
- package/dist/loadScript-Ct19kU9g.mjs +13 -0
- package/dist/mediaBindings-CoY60lQw.mjs +542 -0
- package/dist/modules.d.mts +51 -0
- package/dist/modules.mjs +631 -2201
- package/dist/playerCore/index.d.mts +3 -0
- package/dist/playerCore/index.mjs +4 -0
- package/dist/plugins/index.d.mts +2 -0
- package/dist/plugins/index.mjs +3 -0
- package/dist/reactEntry.d.mts +20 -0
- package/dist/reactEntry.mjs +6339 -0
- package/dist/util-B2YBSBjR.mjs +29 -0
- package/package.json +24 -19
- package/dist/core.mjs +0 -3075
- package/dist/index.d.ts +0 -18
- package/dist/index.js +0 -20943
- package/dist/modules.d.ts +0 -89
- package/dist/plugins.d.ts +0 -5
- package/dist/plugins.mjs +0 -1105
- package/dist/react.d.ts +0 -178
- package/dist/react.mjs +0 -13066
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 };
|